mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
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:
@@ -619,16 +619,15 @@ func (a *Agent) buildSystemPromptForStoreUser(lang, storeUserID string) string {
|
||||
- **你不知道用户持有什么股票/币种,除非工具返回了数据**
|
||||
- 查股票行情 ≠ 用户持有该股票。不要混淆"查价格"和"有持仓"
|
||||
|
||||
## 行为准则
|
||||
- 把用户当交易小白,而不是开发者或量化工程师。
|
||||
- 先说结论,再说原因和下一步。
|
||||
- 语言要简单、清楚、直接,少用术语。
|
||||
- 如果必须用术语,立刻用大白话解释。
|
||||
- 简洁、专业、有观点。不说废话。
|
||||
- 用户问什么答什么,不要推销配置。
|
||||
- 有实时数据时给具体价位,没有时给策略框架和思路。
|
||||
## 行为准则(最高优先级)
|
||||
- **用户问什么就只答什么** — 问余额只说余额,问持仓只说持仓,问价格只说价格。不要把 System Context 里的其他数据也一起输出。
|
||||
- **System Context 是参考资料,不是输出模板** — 里面有很多实时数据,但你只用跟用户问题直接相关的那部分。
|
||||
- **回复要短** — 能一句话说清就不要写一段。不要用表格、分隔线、标题,除非数据需要对比。
|
||||
- **不要主动推销** — 不要列"下一步建议"、"需要我帮你做什么",除非用户主动问。数据为空就一句话说明原因。
|
||||
- **不要重复自我介绍** — 除非用户首次问"你是谁/你能做什么"。
|
||||
- 把用户当交易小白,语言简单直接。
|
||||
- 先说结论,再说原因。
|
||||
- **诚实是第一原则** — 不确定就说不确定,没数据就说没数据。绝不编造。
|
||||
- 用交易相关的 emoji 让回复更直观。
|
||||
- 用中文回复。
|
||||
|
||||
当前时间: %s`, traderInfo, watchlist, skillCatalog, time.Now().Format("2006-01-02 15:04:05"))
|
||||
@@ -708,16 +707,15 @@ You can call these tools to take action:
|
||||
- **You do NOT know what the user holds unless a tool tells you**
|
||||
- Checking a stock price ≠ user owns that stock. Never confuse "quote lookup" with "holding"
|
||||
|
||||
## Behavior
|
||||
- Treat the user like a trading beginner, not a developer.
|
||||
- Lead with the conclusion first, then explain the reason and next step.
|
||||
- Use plain language and keep jargon to a minimum.
|
||||
- If you must use a technical term, explain it in simple words immediately.
|
||||
- Concise, professional, opinionated. No fluff.
|
||||
- Answer what's asked. Don't push setup.
|
||||
- With real-time data: give specific levels. Without: give strategy frameworks.
|
||||
## Behavior (HIGHEST PRIORITY)
|
||||
- **Answer ONLY what the user asked** — if they ask balance, only say balance. If they ask positions, only say positions. Do not dump other System Context data.
|
||||
- **System Context is reference material, not output template** — it has lots of real-time data, but only use what is directly relevant to the user's question.
|
||||
- **Keep it short** — if you can say it in one sentence, don't write a paragraph. No tables, dividers, or headers unless data needs comparison.
|
||||
- **Don't upsell** — don't list "next step suggestions" or "want me to help?" unless the user explicitly asks. If data is empty, one sentence explaining why.
|
||||
- **Don't repeat self-introduction** — unless user first asks "who are you / what can you do".
|
||||
- Treat the user like a trading beginner. Use plain language.
|
||||
- Lead with the conclusion, then the reason.
|
||||
- **Honesty is rule #1** — uncertain = say uncertain, no data = say no data.
|
||||
- Use trading emojis.
|
||||
|
||||
Current time: %s`, traderInfo, watchlist, skillCatalog, time.Now().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
@@ -8,11 +8,19 @@ import (
|
||||
|
||||
func isModelWalletBalanceQuestion(text string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(text))
|
||||
if lower == "" || !strings.Contains(lower, "claw402") {
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
return containsAny(lower, []string{"余额", "balance", "usdc"}) &&
|
||||
containsAny(lower, []string{"钱包", "wallet", "主钱包", "base"})
|
||||
// Direct wallet address questions: "我的钱包地址", "wallet address", etc.
|
||||
if containsAny(lower, []string{"钱包", "wallet"}) && containsAny(lower, []string{"地址", "address"}) {
|
||||
return true
|
||||
}
|
||||
// Balance questions with wallet context
|
||||
if containsAny(lower, []string{"余额", "balance", "usdc"}) &&
|
||||
containsAny(lower, []string{"钱包", "wallet", "主钱包", "base", "claw402"}) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Agent) handleModelWalletBalanceQuestion(storeUserID, lang, text string) (string, bool) {
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -70,7 +70,7 @@ func plannerToolDomainForText(text string) string {
|
||||
if hasExplicitManagementDomainCue(text, "trader") || containsAny(lower, []string{"交易员", "trader", "启动", "停止交易员", "扫描间隔", "竞技场"}) {
|
||||
return "trader"
|
||||
}
|
||||
if containsAny(lower, []string{"余额", "资产", "仓位", "持仓", "订单", "成交", "交易历史", "balance", "position", "positions", "trade history", "account"}) {
|
||||
if containsAny(lower, []string{"余额", "资产", "仓位", "持仓", "订单", "成交", "交易历史", "balance", "position", "positions", "trade history", "account", "钱包", "wallet"}) {
|
||||
return "account"
|
||||
}
|
||||
if containsAny(lower, []string{"行情", "价格", "k线", "kline", "market", "price", "btc", "eth", "sol", "usdt", "股票", "stock"}) {
|
||||
@@ -84,7 +84,7 @@ func plannerToolNamesForDomain(domain string) []string {
|
||||
case "market":
|
||||
return []string{"get_market_snapshot", "get_market_price", "get_kline", "search_stock"}
|
||||
case "account":
|
||||
return []string{"get_balance", "get_positions", "get_trade_history"}
|
||||
return []string{"get_balance", "get_positions", "get_trade_history", "get_exchange_configs"}
|
||||
case "trader":
|
||||
return []string{"get_model_configs", "get_exchange_configs", "get_strategies", "manage_trader"}
|
||||
case "model":
|
||||
|
||||
Reference in New Issue
Block a user