Files
Agent-System-Backend/internal/runtime/compose.go
T
weichenandCursor 46d5f7b668 Keep artifact ids and byte counts out of the final reply
Why: The composed reply copied the registered artifact id and file size out of the tool summary.
Evidence: go test ./internal/runtime/ -run 'TestComposeUserPromptOmitsArtifactIDs|TestComposeReplyDropsInternalRefs'
Impact: Those tokens are removed from the compose input and from the reply before it is shown.
Refs: feature/final-reply-compose
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 14:02:19 +00:00

139 lines
4.9 KiB
Go

package runtime
import (
"fmt"
"regexp"
"strings"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
// 工件编号和字节数只属于系统记录。收尾模型见过工具摘要后会把它们抄进回复。
var internalRef = regexp.MustCompile(`art_[0-9a-f]{8,}|\d[\d,]*\s*字节`)
const composeFallbackPrompt = `你负责写给用户的最终回复。用户只看这一段,看不到上面的工作过程。
规则:
- 只输出给用户看的一段话。不要写思考过程,不要使用 think 标签,不要写「根据规则」「所以我应该」。
- 直接回应用户的原始请求。先写结果和结论,再写交付了什么、要点是什么、还可以怎么调整。
- 不要复述过程,不要提内部检查、lint、重试、验证器、评分、文件路径、字节数或工件编号。
- 检查结论只用来判断有没有没做完的事。文件已经按用户的要求生成时,不要把评分或版式意见告诉用户。
- 只有用户的请求确实没完成时才说明,用用户能懂的话,不要引用检查原文。
- 不要编造材料里没有的内容。使用与用户相同的语言。`
func (l *LLMRunner) composePrompt(owner string) string {
if l.Prompts != nil {
if body, ok, err := l.Prompts.ModuleBody(owner, "aux.compose.final"); err == nil && ok && strings.TrimSpace(body) != "" {
return body
}
}
return composeFallbackPrompt
}
// composeFinal 在工作循环结束后,用主模型单独写一段直接回答用户的回复。
// 失败或空结果时恢复草稿,不影响这一轮已经成功结束。
func (st *directRun) composeFinal(stream agentllm.StreamFunc, model, notice string, opts agentllm.StreamOptions) bool {
_ = st.rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseFinalizing})
draftIdx := st.demoteLastText()
draft := ""
if draftIdx >= 0 {
draft = st.finalParts[draftIdx].Text
}
msgs := []agentllm.Message{
{Role: agentllm.RoleSystem, Content: st.l.composePrompt(st.rc.Profile().Owner)},
{Role: agentllm.RoleUser, Content: st.composeUserPrompt(draft, notice)},
}
var b strings.Builder
failed := false
for ev := range stream(st.rc.Context(), model, msgs, nil, opts) {
switch ev.Kind {
case agentllm.KindTextDelta:
b.WriteString(ev.Text)
case agentllm.KindError:
failed = true
case agentllm.KindMessageEnd:
st.usageTotal.PromptTokens += ev.Usage.PromptTokens
st.usageTotal.CompletionTokens += ev.Usage.CompletionTokens
st.usageTotal.TotalTokens += ev.Usage.TotalTokens
if b.Len() == 0 {
b.WriteString(ev.Message.Content)
}
}
}
// 有的模型把推理放进 think 再写正文。推理不发给用户;只发出剥离后的正文。
body, _ := splitThinkBlock(b.String())
body = scrubInternal(body)
if failed || body == "" || strings.Contains(body, "<think>") {
if draftIdx >= 0 && draftIdx < len(st.finalParts) && st.finalParts[draftIdx].Type == core.PartText {
st.finalParts[draftIdx].Process = false
}
return false
}
_ = st.rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: st.textIdx, Text: body})
st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: body})
st.textIdx++
return true
}
func (st *directRun) composeUserPrompt(draft, notice string) string {
var b strings.Builder
b.WriteString("用户的原始请求:\n")
if user := strings.TrimSpace(currentUserText(st.rc)); user == "" {
b.WriteString("(无)\n")
} else {
b.WriteString(user)
b.WriteString("\n")
}
b.WriteString("\n工作过程中的草稿回复(不要照抄过程,把它整理成给用户的回复):\n")
if strings.TrimSpace(draft) == "" {
b.WriteString("(无)\n")
} else {
b.WriteString(scrubInternal(draft))
b.WriteString("\n")
}
b.WriteString("\n工具过程:\n")
if len(st.evidence) == 0 {
b.WriteString("(无)\n")
}
for i, ev := range st.evidence {
fmt.Fprintf(&b, "#%d %s %s %s\n", i+1, ev.Name, ev.Status, scrubInternal(ev.Summary))
}
b.WriteString("\n交付物:\n")
names := 0
for _, ref := range st.deliverables {
if ref == nil || strings.TrimSpace(ref.Name) == "" {
continue
}
fmt.Fprintf(&b, "- %s\n", ref.Name)
names++
}
if names == 0 {
b.WriteString("(无)\n")
}
b.WriteString("\n检查结论:\n")
if len(st.checks) == 0 {
b.WriteString("(无)\n")
}
for _, c := range st.checks {
if c.Passed {
fmt.Fprintf(&b, "- 通过:%s\n", scrubInternal(c.Reason))
continue
}
fmt.Fprintf(&b, "- 未通过:%s\n", scrubInternal(c.Reason))
}
if strings.TrimSpace(notice) != "" {
b.WriteString("\n仍未完成(必须在回复里如实说明,不要假装已经完成):\n")
b.WriteString(scrubInternal(notice))
b.WriteString("\n")
}
return b.String()
}
func scrubInternal(s string) string {
s = internalRef.ReplaceAllString(s, "")
s = strings.ReplaceAll(s, "()", "")
s = strings.ReplaceAll(s, "()", "")
return strings.TrimSpace(s)
}