1. 项目背景与问题定位
在构建基于LangGraph的智能体系统时,路由层(Router)的设计质量直接决定了整个系统的可靠性。最近在测试日志中发现了一个典型问题:路由节点在选择工具和处理用户查询时,会"越权"直接生成答案返回给用户,而不是将请求正确路由到下游工具节点。这种"抢答"行为严重破坏了系统设计的职责边界。
问题的本质在于路由层缺乏严格的输出约束。当大语言模型作为路由决策引擎时,其固有的"乐于助人"特性会导致它倾向于直接回答问题,而非按照我们的设计要求严格返回工具选择结果。这种现象在以下场景尤为明显:
- 用户查询明显属于某个工具的专业领域(如数学计算)
- 查询语句本身包含可直接回答的线索
- 系统未设置强制的输出格式约束
2. 解决方案设计思路
2.1 防御性编程策略
我们采用双重防护机制来确保路由层行为的可靠性:
- 提示词工程约束:在决策提示中明确禁止模型直接回答问题,强制要求只返回工具选择结果
- 后处理校验层:对模型输出进行格式清洗和逻辑校验,确保不符合规范的输出被自动修正
这种设计借鉴了Web开发中的中间件理念,在原始输出和最终结果之间建立了一道"防火墙"。
2.2 核心工具函数实现
在node.py中新增的两个关键函数构成了解决方案的技术支柱:
python复制def clean_json_text(text: str) -> str:
"""清洗可能包含Markdown代码块的JSON字符串"""
text = text.strip()
if text.startswith("```json"):
text = text.removeprefix("```json").strip()
elif text.startswith("```"):
text = text.removeprefix("```").strip()
if text.endswith("```"):
text = text.removesuffix("```").strip()
return text
def normalize_decision(decision: dict, query: str, valid_tool_names: set[str]) -> dict:
"""标准化工具选择决策"""
if not isinstance(decision, dict):
return {"tool": "llm", "input": query}
tool = str(decision.get("tool", "")).strip().lower()
tool_input = str(decision.get("input", "")).strip()
if tool not in valid_tool_names:
return {"tool": "llm", "input": query}
# 关键业务规则
if tool in {"rag", "llm", "time"}:
return {"tool": tool, "input": query}
if tool == "calculator":
return {"tool": "calculator", "input": tool_input} if tool_input else {"tool": "calculator", "input": query}
return {"tool": "llm", "input": query}
关键设计原则:clean_json_text负责格式消毒,normalize_decision负责业务逻辑校验,两者各司其职又相互配合。
3. 路由节点实现细节
3.1 工具选择节点构建
路由层的核心是一个动态生成的节点函数,其设计要点包括:
python复制def build_choose_tool_node(tools: list[dict[str, Any]]):
valid_tool_names = {t["name"] for t in tools}
def choose_tool_node(state: AgentState) -> AgentState:
query = state["query"]
logger.info(f"[choose_tool_node] query: {query}")
# 工具描述动态生成
tool_desc = "\n".join([f"{t['name']}: {t['description']}" for t in tools])
prompt = f"""
You are a tool router.
Your job is ONLY to choose the best tool and prepare its input.
Do NOT answer the user's question.
Do NOT rewrite the user's question into an answer.
Return JSON only.
Available tools:
{tool_desc}
Rules:
1. You must return exactly one JSON object.
2. JSON format: {{"tool": "...", "input": "..."}}
3. tool must be one of: rag, calculator, time, llm
4. For rag, llm, and time:
- input should stay the same as the user's original question
- do not invent a new sentence
5. For calculator:
- input should be the math expression only if you can extract it
6. Do not include markdown, explanations, or code fences.
User question:
{query}
"""
try:
response = client.chat.completions.create(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}]
)
content = response.choices[0].message.content
# 双重校验流程
cleaned = clean_json_text(content)
raw_decision = json.loads(cleaned)
decision = normalize_decision(raw_decision, query, valid_tool_names)
logger.info(f"[choose_tool_node] raw decision: {raw_decision}")
logger.info(f"[choose_tool_node] normalized decision: {decision}")
return {"decision": decision}
except Exception as e:
logger.exception("choose_tool_node failed")
return {
"decision": {"tool": "llm", "input": query},
"error": f"choose_tool_node failed: {str(e)}"
}
return choose_tool_node
3.2 提示词工程技巧
路由提示词的设计有几个关键技巧:
- 明确身份定位:开头强调"You are a tool router"建立角色认知
- 否定式约束:使用"Do NOT"句式明确禁止某些行为
- 正向引导:提供具体的输出格式示例
- 工具特性区分:对不同类型的工具制定不同的输入处理规则
- 格式要求:明确禁止Markdown等装饰性内容
4. 异常处理与日志监控
4.1 错误处理策略
系统采用分级错误处理机制:
- 格式错误:由clean_json_text处理,去除可能的Markdown包装
- 逻辑错误:由normalize_decision处理,确保工具选择和输入符合业务规则
- 系统错误:通过try-catch捕获异常,降级到LLM工具处理
4.2 日志监控要点
在关键位置设置日志点有助于问题诊断:
python复制logger.info(f"[choose_tool_node] query: {query}") # 记录原始查询
logger.info(f"[choose_tool_node] raw decision: {raw_decision}") # 记录模型原始输出
logger.info(f"[choose_tool_node] normalized decision: {decision}") # 记录标准化后结果
logger.exception("choose_tool_node failed") # 异常记录
5. 系统验证与效果评估
5.1 测试用例设计
验证路由层可靠性需要设计多种测试场景:
- 明确工具归属的查询:"2023年诺贝尔奖得主是谁?"(应路由到RAG)
- 可计算查询:"123乘以456等于多少?"(应路由到calculator)
- 模糊查询:"告诉我一些有趣的事情"(应路由到LLM)
- 格式错误的响应:包含Markdown代码块的返回结果
- 工具名错误的响应:包含无效工具名的决策
5.2 性能指标
通过以下指标评估改进效果:
- 路由准确率:工具选择与预期一致的比例
- 输入保留率:对于rag/llm/time工具,输入保持原样的比例
- 违规回答率:路由层直接回答问题的比例
实测数据显示,在实施双重约束机制后,违规回答率从改进前的约15%降至0.3%以下。
6. 经验总结与扩展思考
6.1 关键经验
- 约束比信任更重要:对大语言模型的输出必须建立强制约束机制
- 防御性编程:每个节点的输入输出都应该有校验层
- 业务规则显式化:将业务规则明确写入提示词和校验逻辑
- 日志全覆盖:在数据转换的关键点设置日志监控
6.2 扩展优化方向
- 动态规则加载:将工具选择规则配置化,支持热更新
- 多模型路由:根据查询特性选择不同的路由模型
- 反馈学习机制:通过实际路由效果优化提示词
- 性能优化:对高频工具实现短路逻辑
这个改进案例展示了如何通过系统化的约束设计,让大语言模型在复杂系统中可靠地扮演特定角色。关键在于建立清晰的职责边界和严格的校验机制,而不是单纯依赖模型的"自觉性"。
