让LLM老老实实调工具,靠提示词根本不够——阿里P8面试引发的深度思考

阿里P8问:怎么让LLM老老实实调工具?候选人答"提示词写清楚就行"。面试官笑了:“那你写一个我看看。”

我想90%的人栽在这。


01 一个让无数开发者踩坑的经典场景

有的时候,我们不要以为在 Prompt 中写好了"让LLM不乱写参数",它就能按照你的意思来执行。

可能你测试了几个例子觉得没问题,但一上线,就会发现系统直接崩盘。

举个具体的例子:查询天气任务,输入参数是 citydate

我们写了一个自以为没问题的系统提示:

你只能调用 get_weather,参数 city 必须是标准城市名,date 必须是今天或明天。不可以自己编城市和日期。

看起来没问题对吧?但用户问:

“北京后天天气怎么样?”

模型的三种典型"瞎编"行为

错误类型 模型行为 问题所在
不调用工具 直接说:“北京后天晴,15~25℃” 完全跳过工具,自己编答案
参数格式错误 传参 date="后天" 工具根本不支持相对时间表达
自作主张转换 传参 date="2026-04-29" 工具没要求具体日期格式,模型自己猜

这三种错误在实际生产中极其常见,而且模型出错时往往表现得非常自信——它不会告诉你"我不确定",而是直接给出一个看起来完全合理的假参数。


02 问题的根源:我们对LLM太自信了

问题的根源往往不是模型不够聪明,而是我们对"模型会完全按照我们的意思来执行工具调用"这件事太过于自信。

2.1 一个普遍的误解:“好好说话就够了”

许多人遇到这类问题后的第一反应是去改提示词。

包括很多资深开发者也是:

“你是一个专业的工具调用助手,不要编造参数。”

这样我们以为就好了,而且听起来很合理,也确实能在简单场景里凑效。

但这里有一个根本性的认知错误:

大语言模型的输出本质上是概率采样,它并没有"遵守规则"的强制机制。提示词只是在概率分布上推了一把,而不是加了锁。

在复杂场景下,比如:

  • 用户输入模糊
  • 上下文噪音大
  • 工具参数嵌套较深

这把"软推"根本不够用。

2.2 这不是某一家模型的缺陷

这并不是某一家模型的缺陷,而是当前LLM范式的系统性特征。

GPT-4、Claude、Gemini在工具调用上都存在类似的幻觉风险,只是程度和触发条件不同。理解这一点,是走出"提示词万能论"的第一步。


03 第一层防线:优化提示词(软约束)

批判"提示词万能论"之后,我们该怎么做?

承认软约束的局限,并不是说提示词优化没有价值。恰恰相反——它应该是整个约束体系的第一层,但绝不能是唯一层。

3.1 提示词应当像一份"操作合同"

优化过的提示词不仅说明工具"用来干什么",还要清楚标注:

  • 每个参数的类型
  • 每个参数的边界
  • 非法值举例

对比两种写法:

写法 示例 效果
❌ 模糊描述 “city参数是城市名称” 模型可能传入任何形式
✅ 精确约束 “city参数必须是标准英文城市名,如’Beijing’,禁止传入自然语言短语如’我所在的城市’或空字符串” 大幅降低歧义

3.2 Few-shot示例:给模型看"正确答案"

更关键的一步是加入 Few-shot 示例——直接给模型看几个"正确的用户输入 → 正确的工具调用"对照组。

// 示例1:标准查询
用户输入:北京今天天气
正确输出:{"tool": "get_weather", "args": {"city": "Beijing", "date": "today"}}

// 示例2:相对时间处理
用户输入:上海明天天气
正确输出:{"tool": "get_weather", "args": {"city": "Shanghai", "date": "tomorrow"}}

这利用了模型的上下文学习能力(In-Context Learning),能够在边界模糊时锚定正确的行为模式。

但即便如此,它仍然是概率性的,不是确定性的。


04 第二层防线:JSON Schema(硬约束)

要从根本上防止模型输出格式混乱,必须引入机器可读的结构化约束,最主流的方式是 JSON Schema

4.1 JSON Schema 的核心思路

不再用自然语言描述参数,而是用机器能验证的结构来定义。

举例来说,一个天气查询工具的 Schema 可以这样约束:

{
  "type": "object",
  "properties": {
    "city": {
      "type": "string",
      "description": "标准英文城市名,如Beijing"
    },
    "date": {
      "type": "string",
      "enum": ["today", "tomorrow"],
      "description": "只允许today或tomorrow"
    },
    "unit": {
      "type": "string",
      "enum": ["celsius", "fahrenheit"],
      "description": "温度单位"
    }
  },
  "required": ["city", "date"],
  "additionalProperties": false
}

4.2 为什么 additionalProperties: false 至关重要?

这最后一条至关重要。

没有它,模型完全可以在输出里附带一个它自己"觉得有用"的额外字段:

{
  "city": "Beijing",
  "date": "today",
  "note": "用户没说清楚,我猜是摄氏度"  // ← 这个字段会让下游系统崩溃
}

然后下游系统完全不知道该怎么处理这个字段。

4.3 主流平台的支持情况

目前主流模型平台都支持在 API 层面直接传入 JSON Schema:

平台 支持方式 特性
OpenAI response_format + tool_choice 结构化输出
Anthropic tools 定义 + JSON Schema 工具调用约束
Google tool_config 格式验证

某些平台甚至在模型解码阶段就会做格式约束(即所谓的"结构化输出"或"Constrained Decoding"),从生成源头就避免格式违规。

这才是真正的硬约束,是脱离了"祈祷模型听话"范式的工程化手段。


05 第三层防线:校验-修复-重试闭环

兜底不是保险丝,而是设计要求。

即便有了 JSON Schema,也有极端情况:

  • 模型输出了语法上合规但语义上荒谬的参数
  • Schema 验证时因为上游处理问题导致格式被破坏

5.1 成熟生产系统的闭环设计

成熟的生产系统需要一个校验-修复-重试的闭环:

1. 拿到模型输出
       ↓
2. 语法校验(JSON格式是否正确?)
       ↓
3. Schema验证(字段类型、枚举值是否符合?)
       ↓
4. 如果失败 → 自动清洗(去除多余Markdown、修复非法引号)
       ↓
5. 如果清洗后仍不合规 → 重试(把错误信息反馈给模型)
       ↓
6. 超过重试上限 → 降级处理或人工兜底

5.2 重试机制的关键细节

具体来说:

  1. 第一轮:拿到模型输出后先做语法校验,再做 Schema 验证
  2. 自动清洗:如果失败,尝试一轮自动清洗(比如去除多余的 Markdown 标记、修复非法的引号格式)
  3. 重试请求:如果清洗后仍然不合规,则把原始输出和错误信息一起打包,作为新的上下文再次请求模型重新生成
  4. 明确反馈:在新提示中明确指出上一次哪里出了问题
# 重试提示示例
prompt = f"""
上一次的工具调用输出格式有误:
原始输出:{last_output}
错误信息:{error_message}

请修正后重新生成,确保:
1. 输出是合法的JSON格式
2. 所有必填字段都存在
3. 枚举值在允许范围内
"""

5.3 重试上限:防止死循环

这个机制有一个前提:重试次数必须有上限。

超过阈值后应当走降级或人工兜底,否则一个死循环的重试链会造成比原始错误更大的破坏。


06 架构层面:让模型只做它该做的事

上面三层措施——优化软约束、硬约束 Schema、校验修复闭环——放在一起,才构成一套可以落地的组合。

但要让这套组合真正稳健,还需要一个架构层面的清醒认识:

LLM只应当承担"决策"职能,而不应当承担"执行"职能。

6.1 三层分离架构

这个区分在架构上体现为三层分离:

层级 职责 类比
模型层 接收用户意图,判断调用哪个工具、生成哪些参数 决策大脑
框架层 接收模型决策、执行 Schema 校验、调用实际工具、处理重试逻辑、整合结果 执行骨架
工具层 各个具体的业务能力实现,与模型完全解耦 业务肌肉

6.2 三层分离的好处

这种设计的好处在于:

  1. 安全性:模型出错时不会直接影响工具调用的安全性,因为中间有框架层的拦截
  2. 可维护性:工具逻辑的变更也不需要重新调整模型的提示词,因为 Schema 定义了它们之间的接口契约
  3. 可测试性:每一层都可以独立测试,降低调试复杂度

6.3 主流框架的本质

LangChain、LlamaIndex、AutoGen 等主流 AI 应用框架,本质上都在做这件事——把执行层的可靠性责任从模型肩膀上卸下来,交给成熟的软件工程实践。

用户输入 → 模型层(决策:调什么工具、什么参数)
              ↓
          框架层(校验:格式对吗?参数合法吗?)
              ↓
          工具层(执行:实际调用业务逻辑)
              ↓
          框架层(整合:把结果返回给用户)

07 一个还没被充分重视的问题

值得补充的是:以上所有方案在处理**“参数格式"问题上效果良好,但对于"参数语义”**问题的覆盖仍然有限。

7.1 格式 vs 语义

层面 问题 Schema能否解决
格式 city 必须是字符串 ✅ 可以
语义 "上海"和"沪"在业务上等价 ❌ 不能

Schema 可以告诉模型 city 必须是字符串,却无法告诉它"上海"和"沪"在业务上是等价的。

7.2 未来的工程挑战

工具调用可靠性的下一个前沿,或许是语义层面的验证——比如:

  • 实体链接:将用户输入的自然语言映射到标准实体
  • 知识图谱补全:利用知识图谱推断参数之间的隐含关系
  • 领域特定的参数规范化模块:针对特定业务场景的参数预处理

这不是2026年的标配,但可能是2027年必须面对的工程挑战。


最后说一句

控制模型调用工具的问题,不是一个"优化提示词"的问题,而是一个软件工程问题。

认清这一点,才算真正迈过了 LLM 应用开发的第一道门槛。

四层防御体系总结:

  1. 提示词优化(软约束)—— 像写合同一样写Prompt
  2. JSON Schema(硬约束)—— 用机器可读的结构定义参数
  3. 校验-修复-重试(兜底机制)—— 闭环设计而非保险丝
  4. 三层分离架构(架构设计)—— 让模型只做决策,不做执行

当你能从这四个层面系统性思考工具调用的可靠性问题时,你就已经超越了90%的 Agent 开发者。


觉得有用?点个在看再走吧 👍

转发给正在做LLM应用开发的朋友,一起聊聊!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐