🧠 LLM 调用解剖

拆开一次 LLM 调用的"黑盒":token、context window、system/user/assistant 角色,以及让 Agent 成为可能的「工具调用(function calling)」。这是 Agent 推理与行动能力的物理基础。

本章目标

  • 理解 token、context window 如何限制 Agent 的"记忆"容量
  • 掌握 Chat 模型的三种消息角色(system / user / assistant)及其用途
  • 彻底搞懂「工具调用 / function calling」的完整时序——Agent 行动能力的来源
  • 把这些底层机制和后续 LangChain 的 AIMessagebind_tools 源码挂钩

一次 LLM 调用的本质

无论 ChatGPT、Claude 还是 Gemini,一次"聊天"调用的本质是:

输入一串 token(messages)→ 模型预测下一个最可能的 token → 重复直到结束
输入 tokens [system, user, assistant, ...] LLM 预测下一个 token (自回归生成) 输出 tokens assistant 的回复 (或 tool_calls) 多轮对话:把输出追加进输入,再次调用
图 2.1 · LLM 是无状态的:每一轮都要把"完整历史"重新喂进去
⚠️ 最关键的一点:LLM 是无状态的

模型本身不记得上一轮说了什么。所谓"多轮对话",是应用层每次都把历史消息全部重新发一遍。这意味着:

  • 历史越长,每次调用越贵、越慢。
  • 历史不能无限长——受 context window 限制。
  • 这就引出了 Agent 的"记忆管理"难题(OpenCode 的 compaction 机制就是为此而生,见 OC5)。

Token 与 Context Window

Token:LLM 的最小单位

模型不直接读"字",而是读 token。token 介于"词"和"字符"之间:一个英文单词约 1~2 个 token,一个中文字约 1~2 个 token。计费、限流、记忆容量都以 token 为单位。

Context Window(上下文窗口)

模型一次能"看进去"的 token 总数有上限,叫 context window。例如 128K、200K。这个窗口要装下所有东西

Context Window(如 128K tokens) System Prompt 人设/规则 工具定义 对话历史 user + assistant + tool 结果 ↑ Agent 这部分会越积越长 当前用户输入 本轮的问题 /指令 输出预留 模型生成 的空间
图 2.2 · Context Window 是个固定大小的盒子,所有内容要挤进去
💡 Agent 为什么需要"记忆管理"

Agent 每走一步,对话历史就增长一截(思考 + 工具调用 + 工具结果)。跑久了历史会撑爆 context window。生产级 Agent 必须有压缩/裁剪历史的能力——LangGraph 用 Checkpointer + 消息裁剪,OpenCode 用 compaction agent(OC5)。这是工程上的硬约束。

三种消息角色

Chat 模型的输入是一串消息(message),每条消息有一个 role(角色)。这三个角色是整个 Agent 体系的基石(LangChain 的 HumanMessage / AIMessage / SystemMessage 就是对它们的封装,见 LC1):

role谁说的用途LangChain 类
system 开发者设定 给 LLM 定人设、规则、可用工具说明(Agent 的"指令"主要靠它 SystemMessage
user 终端用户 用户的提问/指令 HumanMessage
assistant LLM 模型的回复,可能带 tool_calls(表示要调工具) AIMessage
tool 系统(工具) 把工具执行结果喂回给 LLM ToolMessage

一个完整的"工具调用往返"在消息流里长这样(注意 role 的交替):

📄 一次工具调用的完整消息流
① system   : "你是个助手,可以用 search 工具查天气"   ← 工具定义也在这
② user     : "北京今天天气怎么样?"
③ assistant: "我来查一下"  + tool_calls: [search(location="北京")]   ← LLM 决定调工具
④ tool     : {"temp": 25, "weather": "晴"}                            ← 工具结果喂回去
⑤ assistant: "北京今天 25 度,晴天。"                                  ← LLM 基于结果作答

工具调用(Function Calling)—— Agent 的行动力来源

这是本章最核心的部分。没有工具调用,就没有 Agent。它的工作机制分两步:

第 1 步:告诉模型"你有哪些工具"

每个工具本质是一段 JSON Schema,描述工具名、用途、参数。这段 schema 被放进 system 部分(或专门的 tools 字段)。模型训练时已经学会了"读懂 schema 并在合适时机调用"。

📄 一个工具的 JSON Schema 示例
{
  "name": "search_weather",
  "description": "查询指定城市的天气",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string", "description": "城市名,如'北京'" }
    },
    "required": ["location"]
  }
}

第 2 步:模型输出 tool_calls,应用层执行后回传

模型在回复时,可以选择不直接回答,而是输出一个结构化的 tool_calls(而不是普通文本)。应用层(你的代码)负责:① 真正执行这个工具 ② 把结果用 tool 角色消息喂回去 ③ 再次调用模型让它基于结果继续。

你的应用层 1.发送 messages + tools schema 4.执行 tool 5.回传结果 LLM API 2.输出 tool_calls 真实工具 函数/API/DB 产生真实副作用 ② tool_calls ③ 调用工具 ④ 返回结果
图 2.3 · 工具调用的完整往返:模型只"指路",应用层"走路"
⚠️ 重要认知

LLM 永远不会真的"执行"工具。它只是输出一段"请帮我调用 search_weather(location='北京')"的结构化指令。真正去查天气、改文件、发请求的,是你的应用代码。这个"指路 vs 走路"的分离,正是 Agent 安全控制的基础——OpenCode 的权限系统(OC4)就是卡在这一步:LLM 想调工具,但要不要真执行、要不要先问用户,由应用层决定。

与 LangChain 源码挂钩

上面这些底层机制,在 LangChain 源码里都有对应的封装。这里先建立映射,后面章节会精读:

本节概念LangChain 源码位置在哪精读
消息角色 system/user/assistant/tool langchain_core/messages/base.py:93 LC1
AIMessage 带 tool_calls langchain_core/messages/ai.py:160 LC1
把工具 schema 绑定到模型 chat_models.py:2338 bind_tools() LC3
让模型按 schema 输出结构化数据 chat_models.py:2357 with_structured_output() LC3
模型调用入口 invoke/stream chat_models.py:463 invoke() LC3
🔍 可运行的最小验证

你可以用 OpenAI 兼容的任意 API 验证工具调用机制。下面这段 Python 不依赖任何框架,直接构造请求(pip install openai):

📄 minimal_tool_call.py · 裸调用,看清工具调用的本质 python
# pip install openai
from openai import OpenAI

client = OpenAI(api_key="sk-...", base_url="https://api.openai.com/v1")

# 第 1 步:定义工具(就是一段 JSON Schema)
tools = [{
    "type": "function",
    "function": {
        "name": "search_weather",
        "description": "查询指定城市的天气",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "城市名"}
            },
            "required": ["location"],
        },
    },
}]

# 第 2 步:第一次调用 —— 模型决定要调工具
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "北京天气如何?"}],
    tools=tools,
)
msg = resp.choices[0].message
print("模型输出:", msg)  # 注意 content=None, tool_calls=[...]

# 第 3 步:应用层"真正执行"工具(这里假装执行)
import json
args = json.loads(msg.tool_calls[0].function.arguments)
result = {"location": args["location"], "temp": 25, "weather": "晴"}  # 你的真实逻辑

# 第 4 步:把工具结果用 role="tool" 喂回去,再调一次
resp2 = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "北京天气如何?"},
        msg,                                                # 模型上一轮的 tool_calls
        {"role": "tool", "tool_call_id": msg.tool_calls[0].id,
         "content": json.dumps(result)},                     # 工具结果
    ],
    tools=tools,
)
print("最终回答:", resp2.choices[0].message.content)
# → "北京今天 25 度,晴天。"

这段代码刻意不封装,目的是让你看清:Agent 框架(LangChain/LangGraph/OpenCode)做的所有事,本质上都是在自动化第 2~4 步的循环——自动把工具结果拼回去、自动判断是否继续、自动管理消息历史。

小结

下一章

下一章 F3 · Agent 循环理论,我们把本章的"单次工具调用往返"升级为"思考-行动-观察"的多步循环,画出贯穿全教程的核心图——ReAct 范式与状态机三阶段模型。这是所有框架源码的共同骨架。