我的博客

ToolUse & FunctionCall:从「劝模型别编」到「让模型编不了」

Jul 30, 2026
AgentFunctionCallingToolUseAgentLoop
7分钟
1388字

![[w4.png]]

前言

在前面的《Prompt 约束的失效》相关的文章中。V1~V4版本都约束不够有效。在本篇文章中,我们换另一种思路进行。给模型提供查询数据的方式,让模型自己来调用我们提供的 function查数据,根据数据再来输出。


一、prompt 的四个版本都没拦住

V1 的 prompt没有任何限制,LLM 直接编造歌曲; V2版本加了「禁止编造」之后LLM 加了免责声明; V3 版本加了「没有来源 = 不可用」,LLM 给补充了假设; V4 版本加了few-shot + 拒推模板,模型给出了「由于当前无法访问曲库数据,拒绝凭记忆推荐」,但仍然不太稳定,deepseek:32b模型会输出能克制住,但是qwen:7b模型出现了分裂—开头正常显示了示例里的拒推话术、后半段仍然列了 10 首歌。同一个 prompt,两个模型走了不同的路。


二、Function Call换了一个思路:不给自由文本通道

就像是在前端类比上:Prompt 就像是给前端的 Input 加上了 pattern,但实际上我们要的是 Select,没有就不选。Function call就是Select—不给文本的自由通道,只给预定义函数调用通道,由search_catalog返回了数据就用,返回是空就是无数据。不给模型编造的机会。

三、JSON Schema:给模型的菜单

JSON Schema 告诉模型能调用什么工具,每个工具用什么参数,以及参数的作用。比如下方的 JSON,style定风格,bpm 锁区间,budget做经济约束—这四个组成最核心的检索维度。 为什么用 JSON Schema 而不用 Pydantic呢?因为模型在训练时就会使用大量的 JSON Schema,天然就理解这种格式。而Pydantic 只是 python 生态的一种校验工具并不通用。如果使用 Pydantic ,后续如果换语言也需要替换相应的,不过 Pydantic 可以作为 JSON Schema 校验的第二道防线。

1
{
2
"name": "search_catalog",
3
"description": "根据音乐风格、BPM 范围和预算检索曲库中的候选歌曲",
4
"parameters": {
5
"type": "object",
6
"properties": {
7
"style": {"type": "string", "description": "音乐风格,如电子摇滚、流行"},
8
"bpm_min": {"type": "integer", "description": "最低 BPM"},
9
"bpm_max": {"type": "integer", "description": "最高 BPM"},
10
"budget": {"type": "integer", "description": "年度预算上限,单位元"}
11
},
12
"required": ["style", "bpm_min", "bpm_max", "budget"]
13
}
14
}

四、ToolExecutor:三个边界 case 的工程决策

bool-is-int 陷阱

1
# ❌ 错误:True 也是 int
2
if not isinstance(value, int):
3
return f"参数必须是整数"
4
5
# ✅ 正确:精确匹配
6
if type(value) is not int:
7
return f"参数必须是整数"

在 Python 里isinstance(True, int)返回 True,是应为,bool 是 int 的一个子类,如果LLM传了 bpm_min: true时使用isinstance判断类型是不符合要求的,在我们的测试 case test_bool_is_not_int中有体现这块

1
error = executor.validate_params({"style": "流行", "bpm_min": True, "bpm_max": 150, "budget": 3000},schema,)

同步/异步兼容

1
import inspect
2
3
fn = tool['fn']
4
if inspect.iscoroutinefunction(fn):
5
result = await fn(**arguments)
6
else:
7
result = fn(**arguments)

对于 function 的调用,由于目前数据是 Mock 的,所以直接使用的同步 function,在后续接入 DB 数据之后会调整成异步,所以提前进行了兼容

format_error_for_llm 在参数校验失败后,不直接给用户抛出异常;而是将错误信息追加到 message 中,告诉 LLM参数错了,需要调整后重试

1
工具 search_catalog 调用失败。
2
传入参数:{"bpm_min": "fast", ...}
3
错误原因:参数 'bpm_min' 必须是整数(integer),收到 str: fast
4
请修正参数后重新调用。

五、Agent Loop:手写的 think → act → observe(150 字)

1
for turn in range(self.MAX_TURNS): # MAX_TURNS = 3, 最多进行 3 轮对话
2
reply, _ = await self.client.chat_sync(messages) # LLM 回答数据
3
action = self._parse_action(reply) # 操作
4
5
if action is None: # 自然语言 -> 直接结束
6
return { "replay": replay, ...}
7
8
if action["action"] == "reply": # 直接回复,结束
9
return {"reply": action["content"], ...}
10
11
if action["action"] == "use_tool": # 调用工具 => 执行 => 结果追加 => 继续循环
12
result = await self.executor.execute(action["tool"], action["arguments"])
13
messages.append({...tool, result...}) # tool 和 result 相关字段

三种路径: 1. LLM 直接回复 -> 1 轮调用结束(打招呼、闲聊等) 2. LLM 调用工具 -> 成功 -> 结果给 LLM -> LLM 回复 -> 2轮结束(检索推荐场景) 3. LLM 调用工具 -> 失败 -> 错误给 LLM -> LLM 修正 -> 执行成功 -> LLM 回复 -> 3 轮结束 工具调用 + 可能的错误重试 + 最终回复,目前3 轮足够。

_parse_action容错:LLM输出始终是不稳定的 - 可能是 JSON、可能是 markdown代码块、也可能在 JSON 加解释文字。容错的逻辑:先去掉 markdown 包装、再提取第一个{...},最后使用json.loads进行加载。如果还是失效,最后直接把文本返回。

六、实测对比

用例轮次工具调用模型行为
”找电子摇滚 BPM 130–150 预算 4000”21调 search_catalog → 拿到 2 首匹配 → 自然语言推荐
”你好,你是谁?“10直接 reply
”找电子摇滚预算只要 100”21调 search_catalog → 空列表 → 告知无匹配
对比前面的 prompt约束,纯 prompt 约束是,编造了 10 首歌;使用 ToolUse 之后直接拿到真实数据进行推荐。不是回答的变好了,是回答从”模型的记忆幻觉”变成了”曲库的真实数据”。幻觉率从 100% 降到了 0%。

结论

Prompt 在设计”模型能说什么”。Tool Use 在设计”模型不能说什么”——连通道都没有的东西,不需要约束。幻觉不是靠更长的 prompt 解决的——是靠不给模型编造的机会解决的。

本文标题:ToolUse & FunctionCall:从「劝模型别编」到「让模型编不了」
文章作者:vu-ji
发布时间:Jul 30, 2026
Copyright 2026
站点地图