From bb1885eca1a1f688432512c68836d8191db2587d Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sun, 12 Apr 2026 21:42:59 +0800 Subject: [PATCH] fix(searcher): prevent silent termination when MAX_ITERATIONS reached When tool rounds hit MAX_ITERATIONS, the router was jumping directly to END without giving the LLM a chance to produce a final text reply. Added a `force-summary` node that invokes the LLM without tools, ensuring a coherent Chinese summary is always generated after all search results are collected. Co-Authored-By: Claude Sonnet 4.6 --- langgraph/src/agent/searcher/index.ts | 12 +++-- .../src/agent/searcher/nodes/force-summary.ts | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 langgraph/src/agent/searcher/nodes/force-summary.ts diff --git a/langgraph/src/agent/searcher/index.ts b/langgraph/src/agent/searcher/index.ts index 50f4bf3..c73d99e 100644 --- a/langgraph/src/agent/searcher/index.ts +++ b/langgraph/src/agent/searcher/index.ts @@ -3,12 +3,13 @@ import { AIMessage } from "@langchain/core/messages"; import { SearcherAnnotation, SearcherState } from "./types.js"; import { agentNode } from "./nodes/agent.js"; import { toolExecutorNode } from "./nodes/tool-executor.js"; +import { forceSummaryNode } from "./nodes/force-summary.js"; const MAX_ITERATIONS = 3; function routeAfterAgent( state: SearcherState, -): "tool-executor" | typeof END { +): "tool-executor" | "force-summary" | typeof END { const lastMsg = state.messages[state.messages.length - 1]; const aiMsg = lastMsg as AIMessage | undefined; @@ -20,7 +21,8 @@ function routeAfterAgent( ).length; if (toolRounds >= MAX_ITERATIONS) { - return END; + // 达到上限时,强制生成最终总结而非直接结束 + return "force-summary"; } return "tool-executor"; } @@ -31,9 +33,11 @@ function routeAfterAgent( const builder = new StateGraph(SearcherAnnotation) .addNode("agent", agentNode) .addNode("tool-executor", toolExecutorNode) + .addNode("force-summary", forceSummaryNode) .addEdge(START, "agent") - .addConditionalEdges("agent", routeAfterAgent, ["tool-executor", END]) - .addEdge("tool-executor", "agent"); + .addConditionalEdges("agent", routeAfterAgent, ["tool-executor", "force-summary", END]) + .addEdge("tool-executor", "agent") + .addEdge("force-summary", END); export const searcherGraph = builder.compile(); searcherGraph.name = "Searcher"; diff --git a/langgraph/src/agent/searcher/nodes/force-summary.ts b/langgraph/src/agent/searcher/nodes/force-summary.ts new file mode 100644 index 0000000..524d1ed --- /dev/null +++ b/langgraph/src/agent/searcher/nodes/force-summary.ts @@ -0,0 +1,45 @@ +/** + * Force summary node: called when MAX_ITERATIONS is reached. + * Invokes LLM without tools to produce a final text summary based on all collected search results. + */ +import { createLlm, type ModelMode } from "@/agent/utils/create-llm"; +import { truncateMessages } from "@/agent/utils/truncate-messages"; +import { LangGraphRunnableConfig } from "@langchain/langgraph"; +import { HumanMessage } from "@langchain/core/messages"; +import { SearcherState, SearcherUpdate } from "../types.js"; + +const FORCE_SUMMARY_SYSTEM = `你是深度搜索助手。你已完成所有搜索步骤,现在必须基于已收集的搜索结果生成一份完整的最终回答。 + +## 要求 +- 综合所有已获取的搜索结果和网页内容,直接给出完整回答 +- 结论先行:1-2 句话直接回答核心问题 +- 列出关键数据和重要信息 +- 回答末尾列出参考来源(格式:[编号] 标题 - URL) +- 用中文回复 +- 不要说"我需要更多信息"或建议继续搜索——直接基于已有内容回答 +- 搜索结果已通过卡片展示给用户,文字回复侧重分析和总结,不复述搜索摘要`; + +export async function forceSummaryNode( + state: SearcherState, + config: LangGraphRunnableConfig, +): Promise { + const modelMode = ((config.configurable?.modelMode as string) ?? "auto") as ModelMode; + const llm = createLlm({ modelMode }); + + const truncated = truncateMessages(state.messages); + + // Append an explicit instruction to stop tool usage and summarize + const summaryInstruction = new HumanMessage( + "已完成所有搜索。请立即基于上面收集到的所有搜索结果,生成完整的最终回答。不要再调用任何工具。", + ); + + const messagesWithSystem = [ + { role: "system" as const, content: FORCE_SUMMARY_SYSTEM }, + ...truncated, + summaryInstruction, + ]; + + // Invoke without tools to guarantee a text response + const message = await llm.invoke(messagesWithSystem); + return { messages: [message], timestamp: Date.now() }; +}