AI Agent 开发前置知识:模型对话、工具调用与上下文管理
导读 — 本文介绍 AI 应用基础的知识——大模型 API 交互基础。本文内容基于 OpenAI-compatible Chat API 的请求与返回格式展开——OpenAI 请求规范是目前业界最通用的 API 格式,几乎所有主流模型都兼容此协议。
你将看到:
基础对话:system、user、assistant、tool 消息如何组成上下文。
工具调用:模型如何通过 tool_calls 请求应用端执行函数。
Reasoning / Thinking:思考模型如何与工具调用配合。
Tools、MCP、Skills:Agent 能力通常如何注入到模型上下文中。
Prompt Caching:上下文如何影响缓存命中。
ps:本文采用
Chat Completions API(/v1/chat/completions),是目前最广泛使用的版本。OpenAI后续推出了Responses API(/v1/responses),两者核心逻辑相同,不影响阅读。
一、基础对话参数
temperature、top_p 等参数控制的是生成行为,不在本文讨论范围内;本文关注的是模型实际处理的消息内容。
输入
| 参数 | 说明 |
|---|---|
model |
模型名称 |
messages |
对话历史:system 设定角色、user 用户输入、assistant 模型回复、tool 工具结果 |
tools |
工具定义 |
tool_choice |
工具调用策略:auto 模型自行决定、none 禁用工具、required 必须调用工具、{"type":"function","function":{"name":"xxx"}} 强制调用指定工具 |
输出
| 字段 | 说明 |
|---|---|
message.content |
文字回复 |
message.tool_calls |
工具调用请求 |
message.reasoning_content |
推理过程(仅思考模型),OpenAI 一般不返回此字段,属于其他供应商的扩展字段 |
finish_reason |
结束原因:stop 正常结束、tool_calls 请求调用工具、length 达到 token 上限被截断 |
基础对话
单轮对话
最简单的请求,只有 model 和 messages:
1 | { |
响应中包含最终回答 content(推理模型还会额外返回 reasoning_content):
1 | { |
多轮对话
通过累积 messages 实现多轮对话。每轮请求都要带上完整历史。
1 | { |
响应中模型根据上下文知道自己是厨师角色:
1 | { |
关于历史消息中是否需要携带思考字段的问题
上文的历史消息示例中,我们只保留了 role 和 content。但对于部分思考模型,模型回复中可能还会包含 reasoning_content、reasoning、reasoning_details 等思考字段。那么,这些思考字段是否也需要一并写入历史消息呢?
我查阅了相关资料后发现,各家 API 的处理方式并不完全一致:有的场景需要回传思考字段,有的场景不需要;而且不同平台返回的字段名也不相同。例如,DeepSeek 在 thinking mode 中使用 reasoning_content,并明确区分普通对话和工具调用场景:如果本轮只是普通对话,reasoning_content 不需要参与后续上下文拼接;如果本轮发生了工具调用,则需要在后续请求中继续传回该字段。
通用建议是:不要默认把思考字段写入历史消息。普通多轮对话通常只需要保存模型面向用户的最终回答,也就是 content;只有当具体 API 文档明确要求回传思考字段时,才按对应平台的规则处理。
另外,如果模型没有把思考内容拆成单独字段,而是直接把思考链和最终答案一起放在 content 中,例如:
1 | <think>...</think> |
那么也建议在写入历史消息前先做拆分:思考内容可以单独记录到日志中用于调试,但历史消息里通常只保留“最终回答”。这样可以避免上下文膨胀、思考链污染后续回复,也更符合多数推理服务的推荐用法。
二、工具调用
Function Calling(函数调用)允许模型决定何时调用工具、调用哪个工具,以及传递什么参数。下面这个图很清晰,图来至菜鸟教程。

输入
在基础对话参数的基础上,新增:
| 参数 | 说明 |
|---|---|
tools |
工具定义数组,每个工具包含 name、description、parameters |
tool_choice |
工具调用策略:auto 模型自行决定、none 禁用工具、required 必须调用工具 |
工具结果通过 role: "tool" 消息回传,塞进 messages 中:
| 字段 | 说明 |
|---|---|
role |
固定为 "tool" |
tool_call_id |
关联模型返回的 tool_calls[].id(ps:无需关注,使用模型返回的工具 id 字段) |
content |
工具执行结果(字符串) |
输出
模型返回的 message 中新增:
| 字段 | 说明 |
|---|---|
tool_calls |
工具调用请求数组,每个包含 id、function.name、function.arguments |
finish_reason |
为 tool_calls 时表示模型请求调用工具 |
工具定义
1 | { |
完整示例
以”去北京旅游,先查天气,天气好再查空气质量”为例,展示多轮工具调用的完整流程。
先思考 → 先调用天气工具 → 根据天气结果继续思考 → 再决定是否调用空气质量工具 → 再基于工具结果最终回答
第一轮:发送用户消息
请求:
1 | { |
响应(模型先查天气):
1 | { |
第二轮:天气不错,继续查空气质量
请求(追加第一轮的 assistant 消息和天气工具结果):
1 | { |
响应(模型根据天气结果,决定继续查空气质量):
1 | { |
第三轮:生成最终回答
请求(追加第二轮的 assistant 消息和空气质量工具结果):
1 | { |
响应(模型根据所有结果生成最终回答):
1 | { |
三、思考中调工具
「在思考中使用工具」这一能力在2025年因
DeepSeek、MiniMax等模型的支持而广受关注过。具体是什么意思?请看下面。
以 Claude Code 为例,其执行流程为:思考 → 工具调用 → 再思考 → 输出回答。可以看到,工具调用穿插在两次思考之间——这是大模型「在思考中使用工具」的体现。
在上一章节第二轮的响应中也可以看到,reasoning_content、tool_calls 有值,但 content 为空——模型已完成思考并输出了工具调用请求,但尚未给出最终回答,需等待工具返回结果后再继续推理。

部分支持 thinking / reasoning 模式的模型或 API,允许在生成最终答案之前穿插工具调用。典型流程如下:
1 | 用户请求 |
此时 API 响应通常具有以下特征:
1 | reasoning_content / reasoning 有值:API 暴露的 reasoning 信息,字段名因供应商而异 |
与普通工具调用相比,两者的主要区别如下:
| 模式 | 流程 | 特点 |
|---|---|---|
| 普通 tool calling | 用户请求 → 模型返回 tool_calls → 外部执行工具 → 回传 role:"tool" → 模型继续或最终回答 |
通常不暴露思考字段,也不需要回传思考字段 |
| thinking / reasoning tool calling | 用户请求 → 模型 reasoning → 返回 reasoning 信息 + tool_calls → 外部执行工具 → 回传工具结果 → 模型继续 reasoning → 最终回答 |
会暴露 reasoning 字段;是否需要回传该字段取决于具体 API |
附上DeepSeek正确的一张示意图:

一篇更详细的博文:https://news.qq.com/rain/a/20251204A05XL800
四、Agent 中的 Tools、MCP、Skills
在 Agent 场景下,常见的能力注入方式可以简单分成三类:Tools、MCP、Skills。它们的目标都是增强模型能力,但方式不一样。
| 类型 | 可以理解成 | 常见载体 | 说明 |
|---|---|---|---|
| 工具(Tools) | 直接给模型一组“可调用函数” | tools 字段 |
模型通过 tool_calls 请求调用,真正执行由应用端完成 |
| MCP | 外接工具的统一接口 | tools 字段 |
转成普通工具,和内置工具走同一条链路 |
| Skills(技能) | 按需加载的“说明书/操作手册” | system prompt |
以文本形式注入,模型用 read 工具读取执行 |
工具(Tools)
Tools 就是最直接的函数调用。开发者在请求里通过 tools 字段告诉模型有哪些函数可以用,例如查天气、查汇率、查数据库。模型不会自己执行函数,而是返回 tool_calls,由应用端执行后再把结果回传给模型。OpenAI 官方 Function Calling 文档也把这个流程描述为:发送工具定义、模型返回 tool call、应用执行工具、把工具结果回传、模型继续回答。
MCP 工具
MCP 可以理解成”给 Agent 接外部能力的标准插座”。普通 Tools 通常是你在代码里直接定义的函数,而 MCP 工具可能来自一个外部 MCP Server,比如文件系统、GitHub、数据库、浏览器、企业系统等。
对模型来说,MCP 工具和普通工具没有区别;对应用端来说,背后多了一层 MCP Client/Server 的转发。
Skills(技能)
Skills 更像是“操作手册”或“技能包”,不是一个马上执行的函数。它通常包含一个 SKILL.md 文件,里面写着什么时候使用这个技能、具体怎么做、有哪些注意事项,也可能带脚本、模板、参考资料等文件。
在 openclaw 这类实现里,系统会先把可用 skill 的名字、描述、路径注入到 system prompt 里。模型先根据描述判断是否需要某个 skill;如果需要,再用 read 工具读取对应路径下的 SKILL.md。也就是说,skill 本身不是一个函数调用,而是让模型按需加载一份更详细的说明书。
Anthropic 对 Agent Skills 的介绍也强调了类似思路:Skills 通过”渐进披露”工作,先让模型看到少量信息,只有相关时才加载 SKILL.md 和附加文件,避免一开始把所有内容都塞进上下文。
以下基于 openclaw 源码举例,Agent实际是如何加载skill的。
system prompt 中的 Skills 指令
openclaw 的 buildSkillsSection 函数会生成如下指令(参考 src/agents/system-prompt.ts):
1 | ## Skills (mandatory) |
这段指令明确告诉模型:匹配到 skill 后,用 read 工具读取 <location> 路径的 SKILL.md 文件。
技能目录格式
技能列表以 XML 格式注入 system prompt(参考 src/agents/skills/workspace.ts):
1 | <available_skills> |
每个 skill 包含三个字段:
| 字段 | 作用 |
|---|---|
<name> |
标识 skill |
<description> |
让模型判断是否匹配用户请求 |
<location> |
告诉模型去哪里读 SKILL.md |
工具 vs Skills
| 工具(Tools) | Skills | |
|---|---|---|
| 注册方式 | tools 字段 |
system prompt |
| 模型如何调用 | 返回 tool_calls |
调用 read 工具读取 SKILL.md |
| 执行方式 | 应用端执行函数 | 模型按 SKILL.md 指令操作 |
| 选择方式 | 模型直接选工具名 | 模型匹配描述,再读文件 |
Skills 的本质是结构化的提示词工程,把领域知识存成文件,按需加载到上下文中,让模型用现有工具(如 read、bash)去执行。
再拿 Claude Code 的 Skills 格式举例
Claude Code 同样把 Skills 注入 system prompt,但用 XML 标签包裹 + 纯文本列表的形式:
1 | <system-reminder> |
每个 skill 用 - name: 描述 的纯文本格式,没有嵌套 XML 标签。
两者都用 XML 做结构分隔,内部格式不同,核心思路一致:skill 信息以文本形式注入 system prompt,模型根据描述决定是否使用。
总结:
| 类型 | 注册方式 | 模型感知 |
|---|---|---|
| 工具(Tools) | 定义在请求的 tools 字段中 |
模型决定调用哪个工具、传什么参数,由应用端执行后回传结果 |
| MCP 工具 | 发送前转换为标准工具格式,走 tools 字段 |
对模型来说与普通工具无区别 |
| Skills(技能) | 注入到 system prompt 中,不注册为工具 |
模型根据 prompt 中的描述决定是否使用 |
五、 缓存
OpenAI 的 Prompt Caching 可以简单理解为:如果多次请求开头的 prompt 内容完全一样,OpenAI 可以复用之前已经计算过的前缀,从而降低延迟和成本。
参与缓存的字段
| 字段 | 说明 |
|---|---|
messages |
必须从头完全一致,顺序不能变 |
model |
模型名 |
tools |
工具定义 |
tool_choice |
工具策略 |
temperature / top_p 等 |
静态参数 |
不参与缓存的: user 等 metadata 字段。
前缀匹配机制
缓存匹配的是最终 prompt token 的最长相同前缀。例如:
1 | 请求1: [system, tools, user A, assistant, user B] |
所以想提高缓存命中率,关键是让请求开头尽量稳定:
1 | 稳定放前面: |
回传思考字段对缓存的影响
前文提到,不同模型/API 对思考字段的处理规则不同:有些模型在工具调用场景下要求回传 reasoning_content,有些模型则不要求回传。因此,我们用小米的 mimo-v2.5-pro 做了一个对比实验,场景沿用第二章中的“先查天气,天气不错再查空气质量”。
以下是回传与不回传 reasoning_content 的 token 数据对比:
| 不回传 | 回传 | ||
|---|---|---|---|
| 第一轮 | prompt | 686 | 686 |
| cached | 640 | 640 | |
| 缓存率 | 93.3% | 93.3% | |
| 第二轮 | prompt | 748 | 809 (+61) |
| cached | 704 | 640 | |
| 缓存率 | 94.1% | 79.1% | |
| 第三轮 | prompt | 812 | 916 (+104) |
| cached | 768 | 768 | |
| 缓存率 | 94.6% | 83.8% |
关键差异在第二轮:
- 不回传时 cached=704,回传时 cached=640,少了 64 tokens
- 原因:回传的
reasoning_content文本改变了 assistant 消息的内容,与第一轮缓存的前缀不匹配,导致这部分无法复用 - 不回传时 assistant 消息(只有
content+tool_calls)和第一轮响应一致,前缀匹配更长
这说明,在当前实验中,回传
reasoning_content不仅增加了输入长度,还让可复用的缓存前缀变短了。原因是缓存通常依赖精确前缀匹配:如果在历史消息中插入一段新的reasoning_content,最终 prompt token 序列会在更早的位置与之前请求发生分叉,导致后续部分无法复用缓存。因此,对于当前测试的
mimo-v2.5-pro来说,reasoning_content不适合写入历史消息。不过,这个结论不能直接推广到所有模型。比如 DeepSeek 官方 thinking mode 明确要求:如果某一轮发生了 tool call,该轮 assistant 的
reasoning_content必须在后续请求中完整传回,否则可能报错。
结论:
在当前 mimo-v2.5-pro 实验中,不回传 reasoning_content 的效果更好:prompt 更短,缓存命中率更高。
所以,除非 API 明确要求回传思考字段,否则一般不建议默认把 reasoning_content 写入历史消息。
本文的样例代码为Python,OpenAI 提供了Python下的请求包,下载pip install openai,本文没有介绍此包的用法,而是重点围绕 AI 应用设计/开发所需的模型交互知识。微信公众号读者请点击跳转原文,在文章最后附上测试代码。
六、测试代码
(一)基础对话
1 | import os |
(二)工具调用
1 | import os |