![[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 也是 int2if not isinstance(value, int):3 return f"参数必须是整数"4
5# ✅ 正确:精确匹配6if 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中有体现这块
1error = executor.validate_params({"style": "流行", "bpm_min": True, "bpm_max": 150, "budget": 3000},schema,)同步/异步兼容
1import inspect2
3fn = tool['fn']4if inspect.iscoroutinefunction(fn):5 result = await fn(**arguments)6else: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: fast4请修正参数后重新调用。五、Agent Loop:手写的 think → act → observe(150 字)
1for 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” | 2 | 1 | 调 search_catalog → 拿到 2 首匹配 → 自然语言推荐 |
| ”你好,你是谁?“ | 1 | 0 | 直接 reply |
| ”找电子摇滚预算只要 100” | 2 | 1 | 调 search_catalog → 空列表 → 告知无匹配 |
| 对比前面的 prompt约束,纯 prompt 约束是,编造了 10 首歌;使用 ToolUse 之后直接拿到真实数据进行推荐。不是回答的变好了,是回答从”模型的记忆幻觉”变成了”曲库的真实数据”。幻觉率从 100% 降到了 0%。 |
结论
Prompt 在设计”模型能说什么”。Tool Use 在设计”模型不能说什么”——连通道都没有的东西,不需要约束。幻觉不是靠更长的 prompt 解决的——是靠不给模型编造的机会解决的。