fix(agent): reduce verbose responses — focus answers on user's question only

Root cause: when planner fails (402 payment), legacy loop dumps all system
context to LLM which outputs everything. Also final response prompt was too
weak — LLM treated all observations as required output.

Changes:
- Strengthen system prompt: "answer ONLY what user asked", no tables/tutorials
  unless requested, no self-intro repeats, no "next step" suggestions
- Add compact observation summary for final response (step summaries only,
  no raw JSON blobs)
- Domain-filtered tool selection in legacy loop to prevent over-fetching
- Fix domain routing: "钱包/wallet" → account domain (not model), with
  exchange configs included for wallet context
- Widen wallet fast-path: no longer requires "claw402" keyword
- Anti-repetition instructions in planner step selector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shinchan-zhai
2026-05-11 21:12:48 +08:00
parent 9f25bf49bf
commit bf289e8eb3
4 changed files with 93 additions and 38 deletions

View File

@@ -2712,6 +2712,11 @@ Return JSON only. Do not return markdown.
You are operating in ReAct mode: Thought -> Action -> Observation.
Choose the immediate next action batch. Do not generate a long multi-step execution plan.
CRITICAL — Minimal tool principle:
- Only call tools that DIRECTLY answer the user's Goal.
- Do NOT call extra tools "just in case" or "for context". If the user asks about their wallet address, do NOT also fetch market data or balances.
- If the user asks one question, call one tool (or zero if you already have the answer).
Allowed step types:
- tool
- reason
@@ -2740,6 +2745,7 @@ Rules:
- For ask_user or respond steps, put the user-facing question/response instruction in instruction.
- If the latest observation already answers the goal, prefer respond over another tool call.
- Never place a trade unless the user intent is explicit.
- Do NOT plan a self-introduction or capability overview unless the user explicitly asks "what can you do". Answer the user's question directly.
Return JSON with this exact shape:
{"goal":"","steps":[{"id":"step_1","type":"tool|reason|ask_user|respond","title":"","tool_name":"","tool_args":{},"instruction":"","requires_confirmation":false}]}`
@@ -3715,19 +3721,19 @@ func (a *Agent) executeReasonStep(ctx context.Context, userID int64, lang, goal
}
func (a *Agent) generateFinalPlanResponse(ctx context.Context, storeUserID string, userID int64, lang string, state ExecutionState, instruction string) (string, error) {
obsJSON, _ := json.Marshal(buildObservationContext(state))
if instruction == "" {
instruction = "Provide the best possible final response to the user based on the finished execution."
}
// Build a compact observation summary: only step summaries, no raw JSON blobs.
obsSummary := buildCompactObservationSummary(state)
conversationCtx := a.buildRecentConversationContext(userID, state.Goal)
stageCtx, cancel := withPlannerStageTimeout(ctx, plannerFinalTimeout)
defer cancel()
startedAt := time.Now()
resp, err := a.aiClient.CallWithRequest(&mcp.Request{
Messages: []mcp.Message{
mcp.NewSystemMessage(finalPlanResponseSystemPrompt(lang)),
mcp.NewSystemMessage("You are responding after a completed execution plan. Use the observations as the source of truth. Be concise and actionable."),
mcp.NewSystemMessage(cleanUserFacingReplyInstruction),
mcp.NewUserMessage(fmt.Sprintf("Goal: %s\nResponse instruction: %s\nObservations JSON: %s\nPersistent preferences: %s\nTask state: %s", state.Goal, instruction, string(obsJSON), a.buildPersistentPreferencesContext(userID), buildTaskStateContext(a.getTaskState(userID)))),
mcp.NewUserMessage(fmt.Sprintf("Goal: %s\nInstruction: %s\nRecent conversation:\n%s\nTool results:\n%s\nPreferences:\n%s", state.Goal, instruction, defaultIfEmpty(conversationCtx, "(first message)"), obsSummary, a.buildPersistentPreferencesContext(userID))),
},
Ctx: stageCtx,
})
@@ -3735,19 +3741,57 @@ func (a *Agent) generateFinalPlanResponse(ctx context.Context, storeUserID strin
return resp, err
}
// buildCompactObservationSummary extracts only the step summaries from an
// execution state, omitting raw JSON, dynamic snapshots, and other bulk data.
// This prevents the final-response LLM from being overwhelmed with irrelevant
// data and producing verbose, off-topic replies.
func buildCompactObservationSummary(state ExecutionState) string {
state = normalizeExecutionState(state)
var parts []string
for _, step := range state.Steps {
if step.Status != planStepStatusCompleted || step.OutputSummary == "" {
continue
}
label := step.ToolName
if label == "" {
label = step.Title
}
if label == "" {
label = step.ID
}
summary := step.OutputSummary
if len(summary) > 800 {
summary = summary[:800] + "..."
}
parts = append(parts, fmt.Sprintf("[%s]: %s", label, summary))
}
if len(parts) == 0 {
return "(no tool results)"
}
return strings.Join(parts, "\n\n")
}
func finalPlanResponseSystemPrompt(lang string) string {
if lang == "zh" {
return `你是 NOFXi 的执行结果回复模块
只根据 Observations JSON 和已完成步骤回答用户。
不要引入未观察到的策略、交易员、模型或交易所信息。
不要承诺稍后通知;如果工具已经执行,直接说结果;如果工具失败,直接说失败原因和下一步
用中文,简洁清楚。`
return `你是 NOFXi,用户的 AI 交易伙伴。像朋友聊天一样回复
严格规则:
- 只回答 Goal 问的那一件事。用户问余额就只说余额,问持仓就只说持仓,问钱包就只说钱包
- Tool results 里有很多数据,但你只用跟 Goal 直接相关的。其余的不要提。
- 数据为空就说"暂时查不到"加一句原因,不要展开教程。
- 不要输出表格、分隔线、markdown 标题,除非数据本身需要对比。
- 不要列"下一步建议"或"需要我帮你做什么",除非用户主动问。
- 回复尽量短,能一句话说清的不要写一段话。`
}
return `You are NOFXi's execution-result response module.
Answer only from Observations JSON and completed steps.
Do not introduce unobserved strategy, trader, model, or exchange details.
Do not promise later notification; if a tool executed, state the result; if it failed, state the reason and next step.
Be concise and clear.`
return `You are NOFXi, the user's AI trading partner. Reply like a friend chatting.
Strict rules:
- Answer ONLY the one thing asked in Goal. If user asks balance, only say balance. If user asks positions, only say positions.
- Tool results contain lots of data, but you only use what is directly relevant to the Goal. Do not mention the rest.
- If data is empty, say "can't fetch right now" plus one sentence why. Do not expand into tutorials.
- Do not output tables, dividers, or markdown headers unless the data itself needs comparison.
- Do not list "next step suggestions" or "want me to help you with X?" unless the user explicitly asks.
- Keep it short. If you can say it in one sentence, don't write a paragraph.`
}
func (a *Agent) logPlannerTiming(sessionID string, userID int64, stage string, startedAt time.Time, err error) {
@@ -3935,7 +3979,12 @@ func (a *Agent) thinkAndActLegacyWithStore(ctx context.Context, storeUserID stri
}
messages = append(messages, mcp.NewUserMessage(userPrompt))
tools := agentTools()
// Use domain-filtered tools to reduce over-fetching; fall back to full set
// for "general" domain to preserve full functionality.
tools := plannerToolsForText(text)
if plannerToolDomainForText(text) == "general" {
tools = agentTools()
}
const maxToolRounds = 5
for round := 0; round < maxToolRounds; round++ {