跳转至

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 序列上的概率分布)。写代码、写报告、做推理,都是这一过程在长序列上的重复。

还需要明确两个限制:

  1. 模型的“知识”来自训练语料,以网络参数的形式固化,不会自动更新;
  2. 模型没有访问外部环境的能力,无法查询数据库、读取文件或执行命令。

模型在推理时只能利用两类信息:参数中固化的知识,以及当前请求携带的上下文。太新的、未公开的、私有数据,它都无法得知,除非这些信息被放进请求的上下文里。后续的工具调用和 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,还可以读取 usagefinish_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 把工具调用设计成模型的一种受控输出格式。整个机制由三部分组成:

  1. 声明工具:Agent 在请求中附带工具列表,每个工具包含名称、用途说明和参数 Schema。模型只“知道”这些工具存在,并不执行它们。工具描述需要写清楚职责和参数含义,否则模型可能选错工具或生成无法执行的参数。
  2. 约束输出:模型的输出被区分为普通文本(content)和工具调用(tool_calls)两类。当模型判断需要外部信息或希望执行动作时,它输出结构化的调用:工具名加参数。
  3. 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、知识库是在此骨架上按需扩展的模块。