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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
60735de884
commit
bb1885eca1
@@ -3,12 +3,13 @@ import { AIMessage } from "@langchain/core/messages";
|
|||||||
import { SearcherAnnotation, SearcherState } from "./types.js";
|
import { SearcherAnnotation, SearcherState } from "./types.js";
|
||||||
import { agentNode } from "./nodes/agent.js";
|
import { agentNode } from "./nodes/agent.js";
|
||||||
import { toolExecutorNode } from "./nodes/tool-executor.js";
|
import { toolExecutorNode } from "./nodes/tool-executor.js";
|
||||||
|
import { forceSummaryNode } from "./nodes/force-summary.js";
|
||||||
|
|
||||||
const MAX_ITERATIONS = 3;
|
const MAX_ITERATIONS = 3;
|
||||||
|
|
||||||
function routeAfterAgent(
|
function routeAfterAgent(
|
||||||
state: SearcherState,
|
state: SearcherState,
|
||||||
): "tool-executor" | typeof END {
|
): "tool-executor" | "force-summary" | typeof END {
|
||||||
const lastMsg = state.messages[state.messages.length - 1];
|
const lastMsg = state.messages[state.messages.length - 1];
|
||||||
|
|
||||||
const aiMsg = lastMsg as AIMessage | undefined;
|
const aiMsg = lastMsg as AIMessage | undefined;
|
||||||
@@ -20,7 +21,8 @@ function routeAfterAgent(
|
|||||||
).length;
|
).length;
|
||||||
|
|
||||||
if (toolRounds >= MAX_ITERATIONS) {
|
if (toolRounds >= MAX_ITERATIONS) {
|
||||||
return END;
|
// 达到上限时,强制生成最终总结而非直接结束
|
||||||
|
return "force-summary";
|
||||||
}
|
}
|
||||||
return "tool-executor";
|
return "tool-executor";
|
||||||
}
|
}
|
||||||
@@ -31,9 +33,11 @@ function routeAfterAgent(
|
|||||||
const builder = new StateGraph(SearcherAnnotation)
|
const builder = new StateGraph(SearcherAnnotation)
|
||||||
.addNode("agent", agentNode)
|
.addNode("agent", agentNode)
|
||||||
.addNode("tool-executor", toolExecutorNode)
|
.addNode("tool-executor", toolExecutorNode)
|
||||||
|
.addNode("force-summary", forceSummaryNode)
|
||||||
.addEdge(START, "agent")
|
.addEdge(START, "agent")
|
||||||
.addConditionalEdges("agent", routeAfterAgent, ["tool-executor", END])
|
.addConditionalEdges("agent", routeAfterAgent, ["tool-executor", "force-summary", END])
|
||||||
.addEdge("tool-executor", "agent");
|
.addEdge("tool-executor", "agent")
|
||||||
|
.addEdge("force-summary", END);
|
||||||
|
|
||||||
export const searcherGraph = builder.compile();
|
export const searcherGraph = builder.compile();
|
||||||
searcherGraph.name = "Searcher";
|
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() };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user