
开篇
丹尼尔:蛋兄,好久没跟你唠嗑了
蛋先生:确实!我最近发现了一个挺有意思的规律
丹尼尔:什么规律?
蛋先生:每当大模型的能力上升一个台阶,围绕它的 agent 工程代码就会废掉一大片
丹尼尔:怎么说?
蛋先生:要说大模型哪些能力最重要,一个是工具调用(Tool Calling),一个就是思维链(Chain of Thought,简称 CoT)了
丹尼尔:这两个能力确实重要,大模型一开始就有吗?
蛋先生:想多了!早期的大模型就像个只会聊天的话痨,干不了实事
丹尼尔:那当时是怎么解决的呢?
蛋先生:聪明的人类发明了 Prompt Engineering,在提示语里做手脚,在 agent 工程中"外挂"这些能力
丹尼尔:怎么个外挂法?能举个栗子吗?
工具调用的进化
蛋先生:当然,先说下工具调用。如果你要让早期的大模型调用工具,你得这样写(仅仅是一个最简的示例):
css
你是一个工具调用助手。当你需要调用工具时,请输出JSON格式:{tool:工具名,args:{参数名:值}}。
可用工具:get_weather(city) - 查询城市天气。
请查询北京今天天气。
如果大模型发挥正常,它会输出这样的响应:
css
{tool: "get_weather", args: {city: "北京"}}
然后你就可以对这个输出做解析,提取 tool 和 args 字段,再写代码调用对应的工具,这样就成功给大模型"外挂"上工具调用的能力了
当然,肯定会有聪明的程序员把整个过程给工程化了,比如 LangChain 的 create_structured_chat_agent
丹尼尔:工具调用的实现,原来如此
蛋先生:恩,不过大模型有时候可能会皮一下,比如多这么一嘴:
css
好的,我将调用以下工具:
{tool: "get_weather", args: {city: "北京"}}
丹尼尔:那不是得解析到崩溃?
蛋先生:没错!工程师们就得写各种异常处理代码。但解决这个问题,一点都不性感
丹尼尔:那后来怎么解决的呢?
蛋先生:当这种需求积累到一定程度,有了充足的数据后,聪明的训练师就会开始训练大模型,让它直接学会工具调用能力!现在你只要告诉它有哪些可用的工具,它就能直接输出标准格式的调用指令
丹尼尔:哇塞!那是什么效果?
蛋先生:同样是查询北京天气,现在你不需要教它怎么调用工具,你现在只需要说:
scss
可用工具:get_weather(city) - 查询城市天气。
请查询北京今天天气。
丹尼尔:那模型怎么响应?
蛋先生:它会输出类似这样的响应(这里用了 gpt-oss-20b 的模型来演示):
ini
<|channel|>...<|start|>assistant<|channel|>commentary to=tool.get_weather <|constrain|>json<|message|>{"city":"北京"}<|call|>
丹尼尔:这个格式,是固定的吗?
蛋先生:是的!这是大模型的固定输出格式(不同大模型有自己的输出格式,这里演示的是 OpenAI 的 harmony 格式),它知道什么时候该调用工具,该怎么调用
思维链的进化
丹尼尔:那思维链又是怎么回事呢?
蛋先生:思维链就是让模型学会"一步一步思考",而不是直接给出答案
丹尼尔:早期也是靠外挂吗?
蛋先生:是的,当时流行 ReAct 风格的提示语,你得教模型怎么思考、怎么行动、怎么观察
丹尼尔:这得写多复杂的提示语啊?
蛋先生:相当复杂!你得定义清楚 Thought(思考)、Action(行动)、Observation(观察)的格式,还得告诉它什么时候该输出 Final Answer
丹尼尔:听起来头都大了,能给个示例吗?
蛋先生:可以,完整的提示语是这样的:
makefile
你可以访问以下工具:
get_weather(city) - 查询城市天气
使用 JSON 格式指定工具,提供 action(工具名)和 action_input(工具输入)。
有效 action 值:"Final Answer" 或 [get_weather]
每次只提供一个 action,格式如下:
Question: 需要回答的问题
Thought: 必须先思考,解释你的推理过程
Action:
```
{
"action": "工具名",
"action_input": "输入"
}
```
Observation: 行动结果
...(这个 Thought/Action/Observation 可以重复 N 次)
Thought: 我知道该如何回答了
Action:
```
{
"action": "Final Answer",
"action_input": "最终回复"
}
```
**重要**: 每次行动前必须先输出 Thought,不允许直接输出 Action。
Begin!
Question: 请查询北京今天天气。
第一轮请求后,大模型会返回这样的响应:
makefile
Thought: 首先,我需要访问 get_weather 这个工具来获得北京今天的天气信息。
Action:
```
{
"action": "get_weather",
"action_input": "北京"
}
```
接着,你得解析工具返回的结果,提取 action 和 action_input 字段,然后写代码调用对应的工具,把结果补充到提示语里,继续给大模型,如下
makefile
... [第一轮的提示语]
Begin!
Question: 请查询北京今天天气。
Thought: 首先,我需要访问 get_weather 这个工具来获得北京今天的天气信息。
Action:
```
{
"action": "get_weather",
"action_input": "北京"
}
```
Observation: 天气状况:多云,温度:20°C,气压:1013hPa
接下来大模型会返回以下响应进行最终的回答,此次对话就以Final Answer结束了。
makefile
Thought: 我现在知道了北京今天的天气情况,是多云的,温度为20°C,气压为1013hPa。
Action:
```
{
"action": "Final Answer",
"action_input": "北京今天天气:多云,温度20°C,气压1013hPa"
}
```
丹尼尔:那后来大模型也学会了思维链能力?
蛋先生:没错!现在主流的大模型(深度思考模型)都自带思维链能力,你根本不用教它怎么思考
丹尼尔:那现在提示语得多简单?
蛋先生:跟之前学会工具调用一样简单!还是那句:
scss
可用工具:get_weather(city) - 查询城市天气。
请查询北京今天天气。
丹尼尔:真的假的?那模型怎么表现?
蛋先生:它会先输出一段思考内容,比如分析用户需求,决定调用哪个工具,然后再输出工具调用指令
丹尼尔:怎么区分思考和调用呢?
蛋先生:模型会用特定标记区分,比如:
rust
<|channel|>analysis<|message|>The user: "可用工具:get_weather(city) - 查询城市天气。请查询北京今天天气。" They want to use the get_weather tool to fetch today's weather for Beijing. We should call get_weather with "北京". The tool returns some data. Let's do that.<|end|><|start|>assistant<|channel|>commentary to=functions.get_weather <|constrain|>json<|message|>{"city":"北京"}<|call|>
丹尼尔:<|channel|>analysis 后面就是思考内容?
蛋先生:对!这个就相当于之前的 Thought,而且是模型自动输出的,不用你教
丹尼尔:那拿到工具返回结果后呢?
蛋先生:你把结果补充到提示语里,
sql
可用工具:get_weather(city) - 查询城市天气。请查询北京今天天气。
<|channel|>analysis<|message|>The user: "可用工具:get_weather(city) - 查询城市天气。请查询北京今天天气。" They want to use the get_weather tool to fetch today's weather for Beijing. We should call get_weather with "北京". The tool returns some data. Let's do that.<|end|><|start|>assistant<|channel|>commentary to=functions.get_weather <|constrain|>json<|message|>{"city":"北京"}<|call|>
<|start|>functions.get_weather<|channel|>commentary<|message|>天气状况:多云,温度:20°C<|end|>
<|start|>assistant
模型会自动进行最终回答,输出类似:
kotlin
<|channel|>final<|message|>北京今天天气:多云,温度约20°C。<|return|>
丹尼尔:但,好像也得处理大模型的输出格式啊
蛋先生:没错。但最大的区别在于,原来定格式的是在 agent 工程端,而现在定格式的是大模型,虽然每个大模型的原始输出格式可能不一样,但大模型服务提供的 API 已经处理了解析工作,所以我们在 agent 工程端就少了很多输入和输出的处理工作了
新的课题
丹尼尔:这么看来,写 agent 工程是越来越轻松了?
蛋先生:表面上看确实如此
丹尼尔:表面上?
蛋先生:大模型学会了思考和调用工具,但它就像一匹脱缰的野马,跑得飞快但方向难控
丹尼尔:对啊,这我倒没想过
蛋先生:所以 agent 工程的核心就变成了 harness------如何驾驭这匹野马,让它朝着正确的方向奔跑
丹尼尔:(°▽°) 原来如此!
蛋先生:没错!这就是新的课题了,哈哈