Skip to content

与模型对话:LLM API 工作模式

本章摘要

LLM API 是 Agent Loop 的"网络层",本质上是消息历史与可用工具的契约。好的框架把各家协议差异收敛在适配层,让 Agent Loop 看不见这些细节。

llm-api-sequence

上一节讲了 Agent Loop:一个不断重复的循环,每一轮循环的背后就是一次 LLM API 调用。模型读消息历史、给出下一步动作,这就是一次完整的"思考"。

本章不会深入 API 的具体设计上(想了解可以参考 LLM API 协议参考),主要为了讲清楚几个核心问题:

  1. 请求-响应的本质是什么
  2. 消息历史怎么组织
  3. 工具调用怎么走通
  4. 各家协议为什么会有差异、为什么框架要加适配层。

一、请求-响应的本质

无论哪家 LLM API,调用形态都可以抽象为:

  • 请求:把"到目前为止发生了什么"(历史消息)和"接下来你有哪些选择"(工具列表)告诉模型
  • 响应:模型给出"我决定做什么"(可能是继续说话,也可能是调用某个工具)

一个最小化的请求包含三件事:

  1. 使用哪个模型——模型版本决定能力边界和价格
  2. 到目前为止的对话历史——messages 列表
  3. 可调用的工具列表——tools,告诉模型"你能做什么"

其他字段(temperature、max_tokens、是否流式)都是控制参数,决定模型的生成风格和长度。本章不深入这些参数的具体数值,因为它们因 API 而异、随版本调整。理解"消息历史 + 工具"这两个核心,就抓住了 LLM API 的灵魂。

二、消息历史:模型唯一的记忆

模型本身没有持久记忆——它每次调用都从零开始读你传给它的消息列表。这意味着 Agent 维护的"对话历史"就是模型的全部视野

  • 用户的原始任务是什么
  • 模型之前每一轮的思考、行动、观察
  • 工具返回了什么结果

消息历史通常是按时间顺序排列的列表,每条消息带有"角色"和"内容"。 Agent Loop 每跑一轮,就往这个列表里追加一组"思考+行动+观察",然后整体作为下一次请求的输入。

其中,角色的定义一般包含:

角色谁说的作用
system开发者定义模型的行为边界
user用户提出需求或问题
assistant模型模型的回复/工具调用
tool工具执行结果把工具的执行结果回传给模型

一段"三轮对话"的消息历史长这样(用户提需求 → 模型调工具 → 工具回结果 → 模型总结):

jsonc
{
  "messages": [
    // 1. 用户发起请求
    { "role": "user", "content": "统计 sales.csv 中 2024 年 Q1 的总销售额" },

    // 2. 模型返回工具调用请求
    { "role": "assistant", "content": null,
      "tool_calls": [{ "id": "c1", "function": { "name": "read_file",
        "arguments": "{\"path\": \"/data/sales.csv\"}" }}] },

    // 3. 系统返回工具调用结果
    { "role": "tool", "tool_call_id": "c1",
      "content": "日期,销售额\n2024-01,10000\n2024-02,12000\n2024-03,15000" },

    // 4. 模型根据结果给出回答
    { "role": "assistant", "content": "Q1 总销售额 37000 元。" }
  ],
  
  // 工具定义(下一节详细描述)
  "tools": [{
    "type": "function",
    "function": {
      "name": "read_file",
      "description": "读取指定路径的文件内容",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "文件的绝对路径" }
        },
        "required": ["path"]
      }
    }
  }]
}

下一轮 Loop 启动时,Agent 把这四行原封不动作为 messages 字段发给模型,模型就知道之前发生了什么。

这就是为什么"上下文管理"会成为 Agent 系统里极其重要的一章——消息列表越长,模型视野越广,但成本和延迟也越高。当列表超出模型的窗口限制时,Agent 必须主动决定"哪些要保留、哪些要压缩、哪些要丢弃"。

三、工具调用:让模型"动手"

只会生成文本的模型是个聪明的聊天者,但不会影响世界。工具调用补上了"动手"的能力——它允许模型在响应中声明要调用哪个工具、传什么参数,由 Agent 框架负责真正执行,然后把结果回填到消息历史里。

一个工具的描述通常包含三件事(详见上一小节的示例):

  • 名称:工具叫什么
  • 描述:它是做什么的(给模型读的)
  • 参数 schema:调用它需要什么参数、参数是什么类型

模型在响应中不直接执行工具,而是声明它要调用。框架(系统)收到声明后才真正执行(权限检查、解析参数、调用底层 API),然后把执行结果以"工具消息"的身份塞回消息历史。这种"声明-执行-回填"的三段式,让权限和审计成为可能——这是后续"权限管线"章节的基础。

工具调用是 Agent 区别于聊天机器人的关键能力,也是后续"工具系统""权限管线"等章节展开的原点。

四、流式响应:边生成边观察

模型生成回复是一个 token 一个 token 进行的。流式响应允许服务端在生成过程中边产出边推送给客户端,而不是等全部生成完一次性返回(一般使用 SSE 协议,Server-Sent Events,不是本章的重点,不再展开详情)。

对 Agent Loop 来说,流式不影响"思考-行动-观察"的循环逻辑——循环的每一步仍然以"完整的一次 API 调用"为单位。但流式改变了用户和框架能多早看到结果

  • 边生成边展示给用户,体感更流畅
  • 边生成边检测停止信号(比如模型决定调用工具),可以更早进入下一步

一个常见的误解是把"流式"和"循环"混为一谈。它们是两件事:循环是 Agent 的工作节奏,流式是网络层的传输优化。Agent Loop 的每一步仍然以"一次完整调用"为单位,流式只是让这一次的中间过程可见。

五、协议差异:为什么会有不同

不同 LLM 厂商的 API 协议不完全一样——消息结构、工具声明格式、流式事件类型、错误码、安全 Header 都不相同。

为什么会这样?因为 LLM 还在快速演化,协议也在随之调整。早期的协议没考虑工具调用,后来的协议加入多模态、再加入结构化输出、再加入提示词缓存……每家厂商的演进路径不同,最终协议形态也不同。

这意味着同一段 Agent 代码,往往不能直接跑在两个不同的 LLM 厂商上——除非有适配层做转换。

六、适配层:框架的翻译官

正因为协议有差异,一个成熟的 Agent 框架会做一件事:把各家 API 收敛到框架内部的统一接口上

框架写一次,模型换一家就跑——这是适配层的核心价值。

具体做法通常是:

  • 框架内部定义"统一消息""统一工具"等抽象
  • 每个 LLM 厂商有一个适配器,负责把框架抽象翻译成该厂商的 API 格式
  • Agent Loop 只和框架的抽象打交道,从不直接碰任何厂商的私有字段

这个设计让 Agent 系统的代码可以独立于具体模型演化。今天用 A 厂商,明天想换 B 厂商,改的是适配器,不是 Agent 逻辑。

本章要点

  • LLM API 的本质是消息历史 + 可用工具的契约
  • 模型没有持久记忆,消息列表就是它的全部视野
  • 工具调用是"声明-执行-回填"三段式,给权限和审计留出空间
  • 流式是网络层优化,不影响 Agent Loop 的循环结构
  • 协议差异是历史演进的产物,适配层让 Agent 代码与具体模型解耦