Agent 的架构:从文本生成到工具调用¶
为了帮助你理解 Agent 是怎么工作的,这一章从语言模型的基本原理讲起,逐步说明工具调用(Tool Call)、Agent 循环(Agent Loop),以及 MCP、Skills、知识库这些常见概念。
一、大语言模型的基本原理¶
大语言模型(LLM)的核心是一个 Transformer 网络。它的功能可以概括为:给定一段 Token 序列,预测下一个 Token 的概率分布。
例如输入“今天天气真”,模型输出的不是一个确定的词,而是一组候选 Token 及概率,例如:
- “好”,概率约 0.80
- “棒”,概率约 0.15
- “差”,概率约 0.05
模型按这个分布采样得到下一个 Token,把它拼回输入序列,再预测下一个 Token,如此逐步生成。这种方式称为自回归生成(autoregressive generation):
输入: 今天天气真
采样: 好 -> 序列: 今天天气真好
采样: ,适合 -> 序列: 今天天气真好,适合
采样: 出门 -> ...
因此,模型对外唯一的输出通道是文本(更精确地说,是 Token 序列上的概率分布)。写代码、写报告、做推理,都是这一过程在长序列上的重复。
还需要明确两个限制:
- 模型的“知识”来自训练语料,以网络参数的形式固化,不会自动更新;
- 模型没有访问外部环境的能力,无法查询数据库、读取文件或执行命令。
模型在推理时只能利用两类信息:参数中固化的知识,以及当前请求携带的上下文。太新的、未公开的、私有数据,它都无法得知,除非这些信息被放进请求的上下文里。后续的工具调用和 Agent 循环都建立在这条原理上:要让模型获取外部信息或执行动作,必须由 Agent 提供途径。
二、直接调用 OpenAI Completions API¶
在 Agent 出现之前,开发者通常直接调用模型 API。以 OpenAI 的 Completions API 为例,它把模型看成一个“文本续写函数”:程序提交一段 prompt,API 返回模型生成的文本。最直接的用法就是自己写脚本发送 HTTP 请求:
curl https://api.openai.com/v1/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "某个支持 Completions 的模型",
"prompt": "请分析下面这场比赛的 Ban-Pick 思路:\n比赛信息:xxxx\n 版本信息:xxxx\n",
"max_tokens": 800,
"temperature": 0.2
}'
API 返回一段 JSON,脚本需要自己取出 choices[0].text,还可以读取 usage 和 finish_reason:
{
"choices": [
{
"text": "这场比赛中,蓝色方首先禁用了……",
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 42,
"completion_tokens": 96,
"total_tokens": 138
}
}
这里的 API 只负责一次文本生成。它不会替你查询比赛、读取本地知识库、保存多轮上下文,也不会判断接下来是否要调用另一个接口。开发者需要自己写脚本把步骤串起来:
读取用户输入
-> 拼接 Prompt
-> 调用 /v1/completions
-> 解析 choices[0].text
-> 调用比赛 API 获取数据
-> 把比赛数据再次拼进 Prompt
-> 再次调用模型
-> 解析、校验并保存最终报告
如果要支持多轮对话,脚本要手动保存历史并拼接新的 Prompt;如果要调用多个外部系统,脚本要自己判断下一步;如果要限制费用,脚本还要累加每次响应里的 Token 用量。模型只负责生成文字,任务的控制流、错误处理、重试和停止条件都由开发者编写。
这种方式并不是不能用。任务步骤固定、只需要一次文本生成时,直接调用 API 反而最简单。例如,把一段已经准备好的比赛数据转换成摘要,普通脚本就足够了。但当步骤数量不确定,或者模型需要根据中间结果选择下一项动作时,手工脚本会迅速变得复杂:既要维护上下文,又要解析模型输出,还要把模型输出映射到实际函数。
需要注意的是,Completions API 目前属于旧式(legacy)接口;OpenAI 的新文本生成文档主要介绍 Responses API。本节使用它,是为了说明“直接调用模型 API”和“Agent 运行时”之间的区别,而不是建议新项目一定选择这个接口。OpenAI Completions API 文档中可以查看该接口的请求和响应字段。
Agent 方便的地方,正是把这些反复出现的编排工作集中起来:它保存上下文,向模型声明可用工具,解析模型返回的结构化请求,执行工具并把结果回传,再根据停止条件决定是否继续。于是,开发者不必为每个场景重新手写一套“调用模型 -> 解析文本 -> 调用外部 API -> 再调用模型”的脚本,而是把外部能力注册成工具,把任务流程交给统一的 Agent Loop 管理。下一节的工具调用,就是这个运行时把“模型想做什么”转换成“程序可以执行什么”的关键机制。
三、工具调用的原理¶
为了让模型能够“请求”外部能力,主流 LLM API 把工具调用设计成模型的一种受控输出格式。整个机制由三部分组成:
- 声明工具:Agent 在请求中附带工具列表,每个工具包含名称、用途说明和参数 Schema。模型只“知道”这些工具存在,并不执行它们。工具描述需要写清楚职责和参数含义,否则模型可能选错工具或生成无法执行的参数。
- 约束输出:模型的输出被区分为普通文本(
content)和工具调用(tool_calls)两类。当模型判断需要外部信息或希望执行动作时,它输出结构化的调用:工具名加参数。 - Agent 执行:模型不拥有执行环境。是否执行、如何执行、是否允许,都由 Agent 决定。它读取模型返回的
tool_call,校验参数与权限,执行实际动作,然后把执行结果作为一条消息追加到对话历史,再次请求模型。
工具调用是一个“请求-执行”协议,由模型生成请求,Agent 负责执行,执行结果通过消息回传。权限与副作用控制全部在 Agent 一侧。一个工具的实现可以是 HTTP API 调用、数据库查询、本地文件读写,也可以是确定性函数;“工具”描述的是模型可请求的能力接口,而非背后的具体技术。
四、一次 bash 调用的完整流程¶
用 bash 工具为例,把上面的机制落到具体的消息序列上。
1. 声明工具¶
请求中附带工具定义(此处为简化格式,各 API 外层字段略有差异):
{
"name": "bash",
"description": "在用户的电脑上执行一条 shell 命令,例如 ls、cat、运行脚本。",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的命令"
}
},
"required": ["command"]
}
}
2. 模型返回工具调用¶
用户提问“当前目录有什么文件”。模型认为需要外部信息,于是响应中包含 tool_calls 字段,而不是普通文本:
{
"content": "",
"tool_calls": [
{ "name": "bash", "arguments": { "command": "ls -la" } }
]
}
这里 content 为空,说明模型没有直接回答,而是请求调用工具。
3. Agent 执行并回传结果¶
Agent 解析 tool_call,检查该命令是否在允许范围内,然后在本地执行 ls -la,将标准输出作为 tool 角色消息追加到消息历史:
[user] 当前目录有什么文件?
[assistant] 让我看一下。 tool_calls: bash(command="ls -la")
[tool] README.md src/ Cargo.toml
4. 再次请求模型¶
Agent 把全部消息(含上一条 tool 结果)再次发送给模型。模型看到命令输出,据此生成最终回答:
[assistant] 目录下有 README.md、src/ 和 Cargo.toml,看起来是一个 Rust 项目。
一次工具往返至此完成。整个过程中,模型从未直接访问文件系统;它只产生了“调用 bash”这一个结构化的文本请求。
五、Agent 循环¶
单次往返只能完成一个动作。真实任务通常需要多步:先看目录,再读某个文件,然后修改代码,最后运行测试,每一步依赖上一步的结果。为此需要把上述流程放入循环:
循环,直到满足停止条件:
1. 向模型发送全部消息(历史 + 工具结果 + 当前问题);
2. 模型返回普通文本或 tool_call;
3. 若是 tool_call:Agent 执行,并把结果作为 tool 消息追加;
4. 若是普通文本:将其作为最终答案,结束循环。
这个“模型提议、Agent 执行、结果回传、再次请求”的循环称为 Agent Loop。
循环必须有明确的停止条件:模型给出最终答案、达到最大步数、用户取消、达到 Token 或费用预算。缺少停止条件的循环可能反复调用同一工具,造成无效花费与副作用。
六、常见概念¶
除了 Agent 以外,你可能还会接触到很多相关的概念,这里也做一个简单的介绍:
MCP(Model Context Protocol)¶
N 个 Agent 对接 M 个服务,没有统一标准时,每个组合都要单独写对接代码。MCP 是一个开放标准,把工具暴露方式统一成一个接口:服务方按协议暴露工具,Agent 按同一个接口接入,不用重复开发,还能动态发现服务方提供了哪些工具。
它只解决“怎么接入”的标准化问题,不改变 Agent 循环本身。课程项目只有一个 Agent、几个工具,直接在代码里注册即可,用不上 MCP。不过,如果你需要的某个工具已经有现成的 MCP Server,那么用 MCP 接入也是可行的,不用自己从头实现。
Skills¶
工具描述“能做什么”,Skills 描述“某类任务应该怎么做”。Skills 的核心价值是上下文管理。
领域知识如果全部写进系统 Prompt,上下文会越来越长,而上下文太长,模型的注意力会下降,回答质量跟着变差。Skills 的做法是分层加载:平时只把 Skill 清单告诉模型,每个 Skill 只有一句话简介,几乎不占上下文;当模型判断当前任务属于某一类时,再按需加载对应的完整说明,例如这类任务按什么步骤做、要注意什么。
一个 Skill 通常是一个目录,内含 SKILL.md 及可选的参考文档、脚本、模板。这样领域知识既能按需生效,又不会让日常上下文臃肿,还便于单独维护。
知识库¶
Skills 补充的是“怎么做”,知识库补充的是“有哪些知识”。模型训练时的知识可能过时,也不包含你的私有资料;而有些资料量很大,比如一本百科全书,不可能全部放进上下文。
既然装不下全部资料,就只能按需检索其中一部分。检索一般以 Tool Call 的形式工作:把它包装成一个工具,模型在需要时用关键词搜索,拿到相关片段后放进上下文,再继续回答。这样既补上了模型不知道的知识,又只占很少的上下文。
资料多的时候,逐个翻找太慢,所以要提前建索引:把资料切块,按关键词或语义向量建立索引,搜索时先在索引里定位,再取回相关片段。这套做法就是 RAG(检索增强生成)。
小结¶
- 语言模型按给定 Token 序列预测下一个 Token,以自回归方式生成文本,唯一的输出通道是文本;
- 为让模型获取外部信息或执行动作,把工具调用设计为一种受控输出,由 Agent 执行并回传结果;
- 把单次往返放入带停止条件的循环,即 Agent Loop,这是 Agent 的基本骨架;
- MCP、Skills、知识库是在此骨架上按需扩展的模块。