fix(searcher): prevent silent termination when MAX_ITERATIONS reached
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 13s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 51s

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 <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-12 21:42:59 +08:00
co-authored by Claude Sonnet 4.6
parent 60735de884
commit bb1885eca1
2 changed files with 53 additions and 4 deletions
+8 -4
View File
@@ -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";
@@ -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<SearcherUpdate> {
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() };
}