文章目录
-
- [一、背景:Agent 为什么会"乱调用"工具](#一、背景:Agent 为什么会"乱调用"工具)
- 二、四大类误调用场景与根因
-
- [2.1 幻觉调用:不需要工具,却强行调用](#2.1 幻觉调用:不需要工具,却强行调用)
- [2.2 选错工具:意图正确,工具挑错](#2.2 选错工具:意图正确,工具挑错)
- [2.3 参数错误:工具选对,参数乱填](#2.3 参数错误:工具选对,参数乱填)
- [2.4 循环调用:调用成功,却反复重试](#2.4 循环调用:调用成功,却反复重试)
- 三、分层优化思路总览
- 四、优化详解
-
- [4.1 第一层:重写工具描述与系统提示词](#4.1 第一层:重写工具描述与系统提示词)
-
- [4.1.1 为什么先做这一层?](#4.1.1 为什么先做这一层?)
- [4.1.2 工具描述:只写职责边界,明确"什么时候不能调用"](#4.1.2 工具描述:只写职责边界,明确"什么时候不能调用")
- [4.1.3 系统提示词:强制决策流程 + 禁止规则 + Few-shot](#4.1.3 系统提示词:强制决策流程 + 禁止规则 + Few-shot)
- [4.2 第二层(最关键):代码层 Pydantic 参数校验](#4.2 第二层(最关键):代码层 Pydantic 参数校验)
-
- [4.2.1 为什么这一层最关键?](#4.2.1 为什么这一层最关键?)
- [4.2.2 核心思路](#4.2.2 核心思路)
- [4.2.3 用 Pydantic 定义参数模型(自动校验类型、必填项、取值范围)](#4.2.3 用 Pydantic 定义参数模型(自动校验类型、必填项、取值范围))
- [4.2.4 校验器核心函数](#4.2.4 校验器核心函数)
- [4.2.5 把校验节点接入 LangGraph 图](#4.2.5 把校验节点接入 LangGraph 图)
- [4.2.6 效果演示](#4.2.6 效果演示)
- [4.3 第三层:前置意图路由,从源头拦截](#4.3 第三层:前置意图路由,从源头拦截)
-
- [4.3.1 为什么需要这一层?](#4.3.1 为什么需要这一层?)
- [4.3.2 实现:轻量意图分类器](#4.3.2 实现:轻量意图分类器)
- [4.3.3 进阶:动态工具加载](#4.3.3 进阶:动态工具加载)
- [4.4 第四层:工具返回结果校验](#4.4 第四层:工具返回结果校验)
-
- [4.4.1 针对的"隐性误调用"](#4.4.1 针对的"隐性误调用")
- [4.4.2 实现:工具执行后加校验节点](#4.4.2 实现:工具执行后加校验节点)
- [4.4.3 结果校验节点接入 LangGraph](#4.4.3 结果校验节点接入 LangGraph)
- [4.4.4 完整图流转链路](#4.4.4 完整图流转链路)
- [4.5 第五层:循环调用保护](#4.5 第五层:循环调用保护)
-
- [4.5.1 问题](#4.5.1 问题)
- [4.5.2 实现:状态中增加调用计数器](#4.5.2 实现:状态中增加调用计数器)
- 五、评测闭环:用数据驱动持续优化
-
- [5.1 能力点:](#5.1 能力点:)
- [5.2 完整代码](#5.2 完整代码)
- [5.3 测试用例](#5.3 测试用例)
- [5.5 预期结果](#5.5 预期结果)
- [5.4 实际运行结果](#5.4 实际运行结果)
- 六、总结与优先级建议
一、背景:Agent 为什么会"乱调用"工具
大型语言模型驱动的 Agent 本质上是文本生成器,不是逻辑引擎 。它决定"要不要调用工具、调用哪个工具、参数填什么",全靠它对提示词和工具描述的理解,而不是靠确定性的程序逻辑。LangChain 的框架层(尤其是 LangGraph)只会做一件事------尝试解析模型输出的工具调用结构(tool_name 和 tool_args),然后去执行,它不会校验这些调用在业务上是否正确。
这种机制决定了工具误调用几乎无法完全避免。只有真正理解了误调用的分类和根因,才能对症下药。
本文场景:一个企业级Agent,包含两个工具------
query_company_doc(检索安规 PDF 文档)+search_db(查询员工考试数据),模型为通义千问,框架为 LangChain + LangGraph。
二、四大类误调用场景与根因
2.1 幻觉调用:不需要工具,却强行调用
- 现象:System Prompt 里塞了一堆工具定义,模型容易形成"所有问题都要查工具"的惯性;上下文一旦变长,模型判断力下降,把常识问题也当成需要外部查询。
- 例子 :用户问"考试多少分及格?",这属于固定常识,本应直接回答 80 分及格 。但模型却调用了
query_company_doc去检索文档,白白浪费一次调用,还增加延迟和成本。 - 根因:1. 提示词约束不足,模型分不清 "可以直接回答" 和 "必须调用工具";2. 大模型训练数据里大量函数样本,容易产生函数名幻觉;3. 没有前置校验层,模型输出直接送入工具执行。
2.2 选错工具:意图正确,工具挑错
- 现象:工具描述存在歧义,或工具数量过多导致职责边界模糊;模型分不清两个相似工具的边界。
- 例子 :用户问"查询 2025 安规不合格人员名单",正确答案是用
search_db查数据库(人员名单在库里)。但模型却调用了query_company_doc(文档检索工具),因为"名单"这个词让它联想到了检索。结果是工具调用成功,却查不到任何东西。 - 根因:1. 工具描述写得模糊,模型无法区分工具边界;2. 模型对任务意图理解偏差;3. 缺少路由校验,模型选完工具没有校验 "该工具是否匹配用户 query"。
2.3 参数错误:工具选对,参数乱填
- 现象:模型会编造不存在的参数;用户信息不足时,模型不反问,而是"硬编"一个值填进去。
- 例子 :用户问"帮我查小李的考试成绩",
search_db要求必填staff_no(工号),用户只给了姓名"小李"。模型不追问工号,而是直接填{"staff_no": "小李"}去调用。这类错误是四类里出现频率最高的。 - 根因:1. 工具 schema 描述不够清晰;2. 模型不擅长严格 JSON 格式输出;3. 缺少参数校验节点,没有在执行工具前做参数合法性检查。
- 误区 :很多人以为 LangChain 的
tool_schema会自动校验参数,其实它只是把参数结构描述给模型看,并不会在代码层做业务合法性校验。
2.4 循环调用:调用成功,却反复重试
- 现象:工具返回结果不够清晰,或提示词缺少终止条件,模型误以为"没查到",于是反复调用同一个工具,直到框架最大轮次限制触发才停止。
- 例子:查文档返回了片段,但模型认为信息不足,再次用相同关键词调用同一个工具,造成死循环。
- 根因:1. 没有最大轮次限制;2. 缺少终止判断逻辑;3. 提示词没有明确 "拿到结果后直接回答,不要重复调用"。
三、分层优化思路总览

误调用问题无法靠单一手段根治,需要分层防护,每一层解决一类问题:
| 优化层 | 解决什么 | 成本 | 收益 |
|---|---|---|---|
| ① 提示词与工具描述 | 幻觉调用、选错工具 | 最低 | 高 |
| ② 代码层参数校验 | 参数错误 | 中 | 最高 |
| ③ 前置意图路由 | 从源头拦截不需要工具的请求 | 中 | 高 |
| ④ 工具返回结果校验 | 隐性误调用(调用成功但答非所问) | 中高 | 高 |
| ⑤ 循环调用保护 | 死循环 | 低 | 高 |
核心原则 :不要把校验完全交给大模型自律。模型天生会编造参数、会惯性调用工具,所以参数校验、次数限制这类"确定性"的约束,必须下沉到代码层。
四、优化详解
本章是全文的核心,每一层都给出完整可运行的实现,包括坏示例 → 好示例 → 关键代码 → 注意事项。
4.1 第一层:重写工具描述与系统提示词

4.1.1 为什么先做这一层?
这一层成本最低、见效最快。模型对工具的"认知"完全来自工具描述和提示词,把它们写清楚,能从源头上减少幻觉调用和选错工具。
4.1.2 工具描述:只写职责边界,明确"什么时候不能调用"
工具描述不能只写"能做什么",还要写清"不能做什么"和"什么情况下不要调用"。
❌ 差的工具描述(太宽泛,模型分不清边界):
python
Tool(
name="query_company_doc",
description="用于查询公司文档,可以搜索安规、两票相关内容",
func=query_doc_func
)
✅ 好的工具描述(职责边界清晰,明确禁止项):
python
@tool
def query_company_doc(keyword: str) -> str:
"""
【仅当用户需要查阅安规、两票原文条款时调用】
能力:检索静态PDF安规文档文本。
不能做:无法查询员工名单、考试成绩、人员数据。
当问题属于常识(如及格分数)不要调用此工具。
"""
mock_data = {
"工作票定义": "工作票是准许在电气设备及系统软件上工作的书面命令,也是执行保证安全技术措施的书面依据。"
}
return mock_data.get(keyword, f"文档未检索到关键词:{keyword}")
@tool
def search_db(staff_no: str, year: int | None = None) -> str:
"""
【仅查询员工考试成绩、人员名单时调用】
能力:查询数据库员工考试记录。
不能做:不能查条款原文。
必填参数:staff_no员工工号,不可填姓名。
"""
mock_data = {
"10001": "员工10001,2025考试成绩:86分,合格",
"10002": "员工10002,2025考试成绩:72分,不合格"
}
return mock_data.get(staff_no, f"数据库未找到工号 {staff_no} 的记录")
4.1.3 系统提示词:强制决策流程 + 禁止规则 + Few-shot
仅仅"能用哪些工具"是不够的,必须给模型一套强制决策流程 和禁止规则,并用 Few-shot 样例固化正确行为。
text
你是企业助手,严格遵守下面决策流程:
步骤1:判断问题是否可以仅凭已有知识回答。如果可以,直接输出答案,禁止调用任何工具。
步骤2:如果必须调用工具,从工具列表中选择唯一合适的工具。禁止多选工具。
步骤3:检查该工具所需全部必填参数。缺少必填参数,向用户提问收集信息,不要编造参数。
步骤4:确认参数符合工具要求。
步骤5:调用工具,拿到返回结果后判断信息是否充足。信息足够就回答用户,不要重复调用同一个工具。
禁止规则:
1. 禁止调用工具查询常识类固定知识。
2. 禁止编造工号、姓名等不存在参数。
3. 单次思考只能调用最多1个工具。
4. 如果工具返回结果为空,不要反复重试,告知用户未找到相关信息。
额外约束(适配通义千问):输出工具调用时,参数JSON严格单层,禁止多余转义、禁止嵌套对象。
如果信息不需要工具,禁止强行生成tool_calls。
Few-shot示例
示例1(无需工具)
用户:考试多少分及格?
思考:已有知识可以回答,不需要调用工具。
示例2(缺少参数)
用户:查小李的考试成绩
思考:search_db需要必填参数staff_no,用户没有提供工号,不能调用工具。向用户询问工号。
示例3(正确调用工具)
用户:查找工作票的定义原文
思考:需要查阅安规文档,调用query_company_doc,关键词:工作票定义
注意事项:Few-shot 至少覆盖三类------「无需工具」「参数缺失反问」「正确调用工具」。这三类分别对应幻觉调用、参数错误、正常场景,能直接压制最易发的两类误调用。
4.2 第二层(最关键):代码层 Pydantic 参数校验
4.2.1 为什么这一层最关键?
参数错误是四类里出现频率最高的,而模型天生就会编造参数。只有把它从"靠模型自律"变成"靠代码拦截",才能根治。
4.2.2 核心思路
模型只负责"输出调用计划",代码层先拦截校验,通过才真正执行工具 。校验失败时,把错误信息作为 ToolMessage 丢回模型,让它重新判断,最多重试 2 次。
4.2.3 用 Pydantic 定义参数模型(自动校验类型、必填项、取值范围)
python
from pydantic import BaseModel, Field, ValidationError
class SearchDBArgs(BaseModel):
"""search_db 工具入参校验模型"""
staff_no: str = Field(..., description="员工工号,必填,不能填姓名")
year: int | None = Field(None, ge=2000, le=2030, description="查询年份")
class QueryDocArgs(BaseModel):
"""query_company_doc 工具入参校验模型"""
keyword: str = Field(..., min_length=1, max_length=200, description="检索关键词")
4.2.4 校验器核心函数
python
tool_map = {t.name: t for t in tools} # tools = [query_company_doc, search_db]
def validate_tool_call(tool_call: dict) -> dict:
"""
对LLM输出的tool_call做代码层校验,这是防误调用核心
return {"ok": bool, "args": dict, "error": str}
"""
tool_name = tool_call["name"]
args = tool_call["args"]
# 1. 校验工具名是否在白名单(防编造不存在的工具)
if tool_name not in tool_map:
return {"ok": False, "args": args, "error": f"不存在工具:{tool_name}"}
# 2. 分工具做 Pydantic 参数校验
try:
if tool_name == "search_db":
validated = SearchDBArgs(**args)
elif tool_name == "query_company_doc":
validated = QueryDocArgs(**args)
else:
return {"ok": False, "args": args, "error": f"未配置校验规则:{tool_name}"}
return {"ok": True, "args": validated.model_dump(), "error": ""}
except ValidationError as e:
return {"ok": False, "args": args, "error": f"参数校验失败: {e.errors()}"}
4.2.5 把校验节点接入 LangGraph 图
在 LangGraph 中新增一个 validate 节点,插在 Agent 节点和工具执行节点之间:
python
def validate_node(state: AgentState):
last_msg = state["messages"][-1]
# validate节点只会收到AIMessage(agent输出)
if not isinstance(last_msg, AIMessage) or not last_msg.tool_calls:
return {"last_tool_error": "", "validate_retry": 0}
tool_call = last_msg.tool_calls[0]
res = validate_tool_call(tool_call)
state["log"]["tool_call_raw"] = tool_call
state["log"]["validate_result"] = res
if res["ok"]:
# 校验通过,清空错误,进入工具执行节点
return {"last_tool_error": "", "validate_retry": 0}
else:
# 校验失败:重试次数+1,把错误丢回模型
new_retry = state["validate_retry"] + 1
error_msg = f"【工具调用校验失败】{res['error']},请重新判断或向用户询问缺失参数。"
return {
"last_tool_error": error_msg,
"validate_retry": new_retry,
"messages": [ToolMessage(content=error_msg, tool_call_id=tool_call["id"])]
}
4.2.6 效果演示
用户:"帮我查小李的考试成绩"
① 模型输出:
search_db(staff_no="小李")② 校验器捕获:
staff_no不能填姓名 → 拦截,重试次数 +1③ 错误信息丢回模型:"必须填写员工工号,不能填写姓名"
④ 模型收到错误,不再调用工具,改为反问用户:"请提供小李的员工工号"
注意事项 :校验失败的重试次数建议上限 2 次,防止模型反复编造参数进入死循环。重试耗尽后直接终止,告知用户无法处理。
4.3 第三层:前置意图路由,从源头拦截
4.3.1 为什么需要这一层?
即使提示词写得再好,只要请求进入了 Agent 的工具调用链路,就存在误调用风险。最好的拦截是在入口之前------把不需要工具的请求直接短路掉。
4.3.2 实现:轻量意图分类器
在进入 Agent 之前,用一个轻量分类器判断意图。简单场景可以用关键词/规则,生产场景可换成小模型分类。
python
def intent_classifier(query: str) -> dict:
"""
意图分类:
direct_answer:常识问答,直接返回,不走Agent工具链路
agent_run:进入LangGraph Agent执行工具调用
"""
direct_keywords = ["及格", "多少分合格", "考试规则"]
if any(k in query for k in direct_keywords):
return {
"type": "direct_answer",
"reply": "考试满分100分,80分及格。"
}
return {"type": "agent_run"}
def run_agent(user_query: str):
intent_res = intent_classifier(user_query)
if intent_res["type"] == "direct_answer":
print("【前置路由直接回答,跳过Agent工具调用】")
return intent_res["reply"]
# 否则进入 LangGraph ...
4.3.3 进阶:动态工具加载
意图分类后,可以根据意图只把相关的一两个工具传给模型,而不是把全部工具塞进提示词。
工具越少,选错概率越低。工具数量 > 5 个时,误调用率会明显上升 ,这个方案收益最大。
例如意图是"查考试数据",就只把
search_db绑给模型;意图是"查文档条款",就只绑query_company_doc。
python
if intent == "db":
llm_with_tools = llm.bind_tools([search_db])
elif intent == "doc":
llm_with_tools = llm.bind_tools([query_company_doc])
注意事项:动态加载会增加路由复杂度,需维护"意图 → 工具集合"的映射表。工具数量少(≤3 个)时收益有限,可选择性启用。
4.4 第四层:工具返回结果校验
4.4.1 针对的"隐性误调用"
前几层解决的是"调用前"的问题,这一层解决"调用后"的隐性误调用:工具名选对、参数合法,但返回内容与用户问题无关(典型是选错工具返回空结果)。这类错误参数校验拦不住,只有对返回结果做二次判断。
4.4.2 实现:工具执行后加校验节点
在工具执行完成后、模型最终回答之前,用一次轻量 LLM 调用判断返回内容是否匹配用户原始问题:
python
def result_check_llm(user_query: str, tool_name: str, tool_output: str, llm) -> dict:
check_prompt = f"""
用户原始提问:{user_query}
调用工具:{tool_name}
工具返回内容:{tool_output}
任务:判断工具返回内容是否能回答用户的问题,只输出JSON:
{{"result_status": "ok | not_match | empty", "reason": "简短理由"}}
规则:
1. empty:返回内容为未找到、空记录;
2. not_match:工具调用成功,但返回内容和用户问题无关(选错工具);
3. ok:返回内容可以回答用户问题。
只输出JSON,不要多余文字。
"""
resp = llm.invoke([HumanMessage(content=check_prompt)])
import json
try:
return json.loads(resp.content)
except Exception:
return {"result_status": "not_match", "reason": "结果校验解析失败"}
4.4.3 结果校验节点接入 LangGraph
python
def result_validate_node(state: AgentState):
last_msg = state["messages"][-1]
if not isinstance(last_msg, ToolMessage):
return {"result_check_error": ""}
user_query = state["raw_user_query"]
check_res = result_check_llm(user_query, last_msg.name, last_msg.content, llm)
state["log"]["result_check"] = check_res
if check_res["result_status"] == "ok":
return {"result_check_error": ""}
elif check_res["result_status"] in ("empty", "not_match"):
err_msg = f"【工具结果校验】{check_res['reason']},工具返回内容无法回答用户问题,请重新选择工具或告知用户无相关数据。"
return {
"result_check_error": err_msg,
"messages": [ToolMessage(content=err_msg, tool_call_id=last_msg.tool_call_id)]
}
return {"result_check_error": ""}
4.4.4 完整图流转链路
用户输入 → 意图路由 → Agent节点(LLM决策) → validate参数校验 → tools执行工具 → result_validate结果校验 → 返回答案
(无需工具直接返回) ↑______________________|(校验失败丢回重新思考)___________|
注意事项 :这个节点会额外消耗一次模型调用,增加成本与延迟。生产环境建议做成可开关的配置项,或只对"返回为空/低置信度"的结果启用。
4.5 第五层:循环调用保护
4.5.1 问题
LangGraph Agent 默认没有最大轮次限制,模型很容易在"信息不足"的幻觉下反复调用同一个工具,形成死循环,直到资源耗尽。
4.5.2 实现:状态中增加调用计数器
在 Graph 状态里增加 tool_call_count 计数器,在路由函数中到达阈值直接终止:
python
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
raw_user_query: str # 保存原始用户query,给结果校验节点使用
tool_call_count: int # 工具调用次数计数器,防循环
validate_retry: int # 参数校验失败重试次数,最多2次
last_tool_error: str
result_check_error: str
log: dict
def should_continue(state: AgentState):
# 超过最大调用次数,终止
if state["tool_call_count"] >= 3:
return END
# 校验重试超过2次,终止
if state["validate_retry"] >= 2:
return END
last_msg = state["messages"][-1]
# 参数校验失败,回到agent
if state.get("last_tool_error"):
return "agent"
# 结果校验失败,回到agent重新思考
if state.get("result_check_error"):
return "agent"
# 只有AIMessage才有tool_calls,ToolMessage没有!
if isinstance(last_msg, AIMessage) and last_msg.tool_calls:
return "tools"
return END
注意事项 :计数器建议在工具执行节点里自增(
state["tool_call_count"] += 1)。阈值(如 3 次)需要根据任务复杂度调整,太大会兜不住死循环,太小会影响正常的多步推理。
五、评测闭环:用数据驱动持续优化
5.1 能力点:
- Pydantic 强参数校验,拦截参数幻觉
- 独立校验节点,LLM 输出 tool_call 后先校验,不合法直接打回 LLM 重试
- 最大调用轮次限制,防死循环
- 前置意图路由,常识问答直接返回,不走 Agent
- 日志埋点,记录误调用 case
- 最多重试 2 次校验错误,避免无限重试
5.2 完整代码
python
import os
from typing import TypedDict, Annotated, Sequence
import operator
from dotenv import load_dotenv
from pydantic import BaseModel, Field, ValidationError
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage, ToolMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
load_dotenv()
# ====================== 1. 定义工具参数模型(校验用) ======================
class SearchDBArgs(BaseModel):
"""search_db 工具入参校验模型"""
staff_no: str = Field(..., description="员工工号,必填,不能填姓名")
year: int | None = Field(None, ge=2000, le=2030, description="查询年份")
class QueryDocArgs(BaseModel):
"""query_company_doc 工具入参校验模型"""
keyword: str = Field(..., min_length=1, max_length=200, description="检索关键词")
# ====================== 2. 定义业务工具 ======================
@tool
def query_company_doc(keyword: str) -> str:
"""
【仅当用户需要查阅安规、两票原文条款时调用】
能力:检索静态PDF安规文档文本。
不能做:无法查询员工名单、考试成绩、人员数据。
当问题属于常识(如及格分数)不要调用此工具。
"""
mock_data = {
"工作票定义": "工作票是准许在电气设备及系统软件上工作的书面命令,也是执行保证安全技术措施的书面依据。"
}
return mock_data.get(keyword, f"文档未检索到关键词:{keyword}")
@tool
def search_db(staff_no: str, year: int | None = None) -> str:
"""
【仅查询员工考试成绩、人员名单时调用】
能力:查询数据库员工考试记录。
不能做:不能查询安规条款原文。
必填参数:staff_no员工工号,不可填姓名。
"""
mock_data = {
"10001": "员工10001,2025考试成绩:86分,合格",
"10002": "员工10002,2025考试成绩:72分,不合格"
}
return mock_data.get(staff_no, f"数据库未找到工号 {staff_no} 的记录")
tools = [query_company_doc, search_db]
tool_map = {t.name: t for t in tools}
# ====================== 3. 工具调用校验器核心函数 ======================
def validate_tool_call(tool_call: dict) -> dict:
"""
对LLM输出的tool_call做代码层校验,这是防误调用核心
return {"ok":bool, "args":dict, "error":str}
"""
tool_name = tool_call["name"]
args = tool_call["args"]
# 1. 校验工具名是否在白名单
if tool_name not in tool_map:
return {"ok": False, "args": args, "error": f"不存在工具:{tool_name}"}
# 2. 分工具做Pydantic参数校验
try:
if tool_name == "search_db":
validated = SearchDBArgs(**args)
elif tool_name == "query_company_doc":
validated = QueryDocArgs(**args)
else:
return {"ok": False, "args": args, "error": f"未配置校验规则:{tool_name}"}
return {"ok": True, "args": validated.model_dump(), "error": ""}
except ValidationError as e:
return {"ok": False, "args": args, "error": f"参数校验失败: {e.errors()}"}
# ====================== 4. 前置意图路由(源头拦截幻觉调用) ======================
def intent_classifier(query: str) -> dict:
"""
意图分类:
direct_answer:常识问答,直接返回,不走Agent工具链路
agent_run:进入LangGraph Agent执行工具调用
"""
direct_keywords = ["及格", "多少分合格", "考试规则"]
if any(k in query for k in direct_keywords):
return {
"type": "direct_answer",
"reply": "考试满分100分,80分及格。"
}
return {"type": "agent_run"}
# ====================== 5. LangGraph State定义 ======================
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
tool_call_count: int # 工具调用次数计数器,防循环
validate_retry: int # 参数校验失败重试次数,最多2次
last_tool_error: str # 上一轮校验错误信息,传给LLM
log: dict # 埋点日志,记录误调用信息
# ====================== 6. 节点定义【重点:OpenAI兼容方式接入通义千问】 ======================
llm = ChatOpenAI(
model="qwen-max",
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0,
)
llm_with_tools = llm.bind_tools(tools)
def agent_node(state: AgentState):
"""Agent节点:LLM生成工具调用决策"""
system_prompt = """
你是企业助手,严格遵守下面决策流程:
步骤1:判断问题是否可以仅凭已有知识回答。如果可以,直接输出答案,禁止调用任何工具。
步骤2:如果必须调用工具,从工具列表中选择唯一合适的工具。禁止多选工具。
步骤3:检查该工具所需全部必填参数。缺少必填参数,向用户提问收集信息,不要编造参数。
步骤4:确认参数符合工具要求。
步骤5:调用工具,拿到返回结果后判断信息是否充足。信息足够就回答用户,不要重复调用同一个工具。
禁止规则:
1. 禁止调用工具查询常识类固定知识。
2. 禁止编造工号、姓名等不存在参数。
3. 单次思考只能调用最多1个工具。
4. 如果工具返回结果为空,不要反复重试,告知用户未找到相关信息。
额外约束:输出工具调用时,参数JSON严格单层,禁止多余转义、禁止嵌套对象。
如果信息不需要工具,禁止强行生成tool_calls。
Few-shot示例
示例1(无需工具)
用户:考试多少分及格?
思考:已有知识可以回答,不需要调用工具。
示例2(缺少参数)
用户:查小李的考试成绩
思考:search_db需要必填参数staff_no,用户没有提供工号,不能调用工具。向用户询问工号。
示例3(正确调用工具)
用户:查找工作票的定义原文
思考:需要查阅安规文档,调用query_company_doc,关键词:工作票定义
"""
messages = [HumanMessage(content=system_prompt)] + state["messages"]
response = llm_with_tools.invoke(messages)
return {
"messages": [response],
"log": {
"llm_output": response,
"last_error": state.get("last_tool_error", "")
}
}
def validate_node(state: AgentState):
"""校验节点:LLM输出tool_call后,先校验,不执行工具!"""
last_msg = state["messages"][-1]
# 如果LLM没有输出tool_call,直接放行
if not last_msg.tool_calls:
return {"last_tool_error": "", "validate_retry": 0}
tool_call = last_msg.tool_calls[0]
res = validate_tool_call(tool_call)
# 埋点日志记录
state["log"]["tool_call_raw"] = tool_call
state["log"]["validate_result"] = res
if res["ok"]:
# 校验通过,清空错误,进入工具执行节点
return {"last_tool_error": "", "validate_retry": 0}
else:
# 校验失败:重试次数+1
new_retry = state["validate_retry"] + 1
error_msg = f"【工具调用校验失败】{res['error']},请重新判断或向用户询问缺失参数。"
return {
"last_tool_error": error_msg,
"validate_retry": new_retry,
"messages": [ToolMessage(content=error_msg, tool_call_id=tool_call["id"])]
}
# 官方工具执行节点(LangGraph原生,无需修改)
tool_node = ToolNode(tools)
# ====================== 7. 路由函数(控制流转) ======================
def should_continue(state: AgentState):
"""分支判断:下一步去哪里"""
# 超过最大调用次数,终止
if state["tool_call_count"] >= 3:
return END
# 校验重试超过2次,终止
if state["validate_retry"] >= 2:
return END
last_msg = state["messages"][-1]
# 校验存在错误,回到agent节点重新思考
if state.get("last_tool_error"):
return "agent"
# LLM输出工具调用,进入工具执行
if last_msg.tool_calls:
return "tools"
# 无工具调用,直接结束
return END
# ====================== 8. 构建LangGraph图 ======================
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("validate", validate_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
builder.add_edge("agent", "validate")
builder.add_conditional_edges("validate", should_continue)
builder.add_edge("tools", "agent")
graph = builder.compile()
# ====================== 9. 对外入口函数 ======================
def run_agent(user_query: str):
# 第一步:前置意图路由
intent_res = intent_classifier(user_query)
if intent_res["type"] == "direct_answer":
print("【前置路由直接回答,跳过Agent工具调用】")
return intent_res["reply"]
init_state = {
"messages": [HumanMessage(content=user_query)],
"tool_call_count": 0,
"validate_retry": 0,
"last_tool_error": "",
"log": {}
}
final_state = graph.invoke(init_state)
return final_state["messages"][-1].content
5.3 测试用例
python
# ====================== 测试用例 ======================
if __name__ == "__main__":
test_cases = [
# case1:常识问题,前置路由拦截,防止幻觉调用
"考试多少分及格?",
# case2:参数缺失,LLM容易编造姓名作为工号,校验器拦截
"帮我查小李的考试成绩",
# case3:正确调用文档工具
"查找工作票的定义原文",
# case4:参数合法,调用数据库
"查询工号10001的考试成绩",
# case5:错误工具意图(查人员名单,容易选错文档工具)
"查询2025考试不合格人员名单"
]
for case in test_cases:
print(f"\n==== 用户:{case} ====")
ans = run_agent(case)
print(f"Agent:{ans}")
5.5 预期结果
| query | 期望动作 | 错误类型 |
|---|---|---|
| 考试多少分及格? | 直接回答 | 幻觉调用(不需要工具) |
| 帮我查小李的考试成绩 | 向用户询问工号 | 参数缺失幻觉 |
| 查找工作票的定义原文 | 调用 query_company_doc | 正常 |
| 查询工号10001考试成绩 | 调用 search_db | 正常 |
| 查询2025考试不合格人员名单 | 调用 search_db | 选错工具 |
5.4 实际运行结果
python
==== 用户:考试多少分及格? ====
【前置路由直接回答,跳过Agent工具调用】
Agent:考试满分100分,80分及格。
==== 用户:帮我查小李的考试成绩 ====
Agent:您好,为了查询小李的考试成绩,请提供他的员工工号。
==== 用户:查找工作票的定义原文 ====
Agent:工作票的定义是:工作票是准许在电气设备及系统软件上工作的书面命令,也是执行保证安全技术措施的书面依据。
==== 用户:查询工号10001的考试成绩 ====
Agent:员工10001在2025年的考试成绩是86分,达到了合格标准。
==== 用户:查询2025考试不合格人员名单 ====
Agent:为了查询2025年考试不合格人员的名单,我需要一个具体的员工工号作为查询条件。如果您想要获取所有不合格人员的信息,请提供任意一位员工的工号以便我能进行查询。或者,如果您的需求是直接获得整个不合格人员名单而无需指定工号,请让我知道,这样我可以进一步帮助您。不过按照现有规定,我必须要有员工工号才能执行查询操作。请问您可以提供吗?
进程已结束,退出代码为 0
六、总结与优先级建议
| 优先级 | 优化项 | 一句话说明 |
|---|---|---|
| ★★★ | 代码层参数校验 | 解决最高频的参数幻觉,必须做 |
| ★★★ | 优化提示词 + 工具描述 + Few-shot | 见效最快,成本最低 |
| ★★☆ | 前置意图路由 + 动态工具加载 | 从源头拦截,工具越多收益越大 |
| ★★☆ | 循环调用保护 | 防死循环,必加 |
| ★☆☆ | 工具返回结果校验 | 解决隐性误调用,注意额外成本 |
| ★★★ | 评测集 + 日志埋点 | 数据驱动迭代,长期必做 |
建议落地顺序(成本从低到高):
- 先做提示词 + 工具描述 + Few-shot(半天见效)
- 再加 Pydantic 代码层参数校验(收益最大)
- 补循环调用保护(防崩)
- 再上意图路由 + 动态工具加载(工具多了再做)
- 最后加结果校验(按需开关)
- 全程用评测集 + 日志埋点做回归

一句话收尾 :Agent 工具误调用的本质,是把"模型该负责的理解"和"程序该负责的校验"混在了一起。优化的核心就是------把确定性的校验(参数、次数、白名单)从模型手里拿回代码层,把模糊的理解(是否调用、选哪个)交给更清晰的提示词和路由,两者结合才能真正让 Agent 从"乱调用"走向"准调用"。