How AI Coding Works I: LLMs, Tools & the Agent Loop
把大语言模型(LLM,Large Language Model)剥到最底层,它就是一个把文本映射到文本的函数:
所谓”调用之间完全独立”,指的是两次调用 和 互不影响——上一次输入了什么、输出了什么,对这一次毫无影响。这是函数式编程里**纯函数(pure function)**的特性。
LLM 的全部知识在训练阶段就被烤进模型权重(parameters)里了。推理(inference)时,模型权重是只读的:每一次 API 调用都加载同一份权重,跑一遍前向计算,输出 token,结束。权重不会因为”你刚才和它聊过”而改变。
这也意味着:模型本身不会”学习”你这次对话的内容。它表现出的”记住”,是外部代码模拟出来的(见聊天记忆)。
很多人觉得”AI 记得我昨天说的话”。从模型层面看,这是错的。真实情况是:调用方(你用的 App、Claude Code 这类 Agent)把昨天的对话重新拼进本次输入一起发给模型。模型每次都是从零开始读这整段输入。
换句话说,“记忆”不在模型里,在每次发送的输入里。
LLM 是无状态的(见无状态函数),那聊天时它怎么”记得”前面说过的话?答案是调用方每次都把完整历史对话重新发一遍。
一个 3 轮的聊天,每次 API 调用实际发送的内容是这样增长的:
第 1 次调用:[user: 你好]
第 2 次调用:[user: 你好, assistant: 你好!, user: 1+1=?]
第 3 次调用:[user: 你好, assistant: 你好!, user: 1+1=?, assistant: 2, user: 再加3呢?]
每一轮都把之前所有问答原样带上,再加上新的提问。模型重新读一遍全部历史,看起来就像”记得”了。
OpenAI / Anthropic 等主流 API 都用一个 messages 数组描述对话,每条消息标明角色(role):
{
"messages": [
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!"},
{"role": "user", "content": "1+1=?"}
]
}
四种角色:
| role | 含义 |
|---|---|
system | 系统指令,定义模型行为(见 System Prompt) |
user | 用户说的话 |
assistant | 模型之前生成的回复 |
tool / function | 工具执行后回传的结果(见工具调用) |
每次都发完整历史,意味着输入长度随对话轮次线性增长,相应地 token 费用和延迟也线性增长。这正是上下文窗口(context window)会成为瓶颈的原因——窗口装不下时,旧的轮次必须被裁掉或压缩(见上下文压缩)。
assistant 角色的消息是历史的复述,不是实时生成的——只有数组最后一条 user 之后模型新生成的内容,才是本次真正”思考”的产物。模型本质是文本到文本的函数(见无状态函数)。它没有任何直接读写文件、执行命令、访问网络的能力——它只会输出 token。
“工具调用”(tool calling / function calling)是让 LLM 操作真实世界的约定:模型不亲自执行,而是输出一段结构化指令,由外部程序解析并执行,再把结果送回来。
模型生成的不是”执行了写文件”,而是一段 JSON,描述”请帮我执行这个工具”。以 OpenAI 风格为例:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\": \"hello.cpp\", \"content\": \"#include <iostream>\\nint main(){...}\"}"
}
}
]
}
注意几个关键点:
tool_calls 是模型在 assistant 消息里输出的字段,本质就是文本(JSON 序列化)。arguments 通常是 JSON 字符串(不是对象),需要外部代码再 JSON.parse 一次。id(如 call_abc123)用来配对:执行结果回传时必须带上同一个 id,模型才知道这是哪次调用的结果。调用 API 时,除了 messages,还要传一个 tools 参数,列出可用工具的名字、参数 schema:
{
"tools": [
{
"type": "function",
"function": {
"name": "write_file",
"description": "把内容写入指定路径的文件",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
}
}
]
}
模型靠这份 schema 知道”有哪些工具可用、参数叫什么”。schema 写得越清楚(描述、类型、必填),模型选对工具、填对参数的概率越高。
外部程序执行 write_file 后,把结果作为一条 tool 角色消息追加进 messages:
{"role": "tool", "tool_call_id": "call_abc123", "content": "已写入 hello.cpp(123 字节)"}
模型在下一次调用时读到这条结果,才会知道操作成功了,进而决定下一步(比如继续编译)。这就是 Agent 循环的每一步。
arguments 是字符串。新手常直接当对象用,导致取值失败。tool_call_id,否则模型无法把结果和它发起的调用对应起来。System Prompt 是每次 API 调用时附带的最高优先级指令,通过 messages 数组里第一条 role: "system" 的消息传入(见消息数组)。它告诉模型:
可以把它想成给新员工的岗位说明书——它定义了模型在这次会话里全部的行为边界。
{
"messages": [
{"role": "system", "content": "你是一个运行在 Linux/bash 环境的编码助手。当前工作目录是 /home/xsl/project。优先使用 g++ 编译 C++ 代码……"},
{"role": "user", "content": "写个 hello world"}
]
}
模型是无状态的(见无状态函数),上一轮发的 system prompt 它”记不住”。所以每次调用都得把 system prompt 重新作为第一条消息带上,否则模型这一轮就不知道自己的角色和环境。
这也是为什么 system prompt 的长度会持续占用 token:它和完整对话历史一起,每次都重新发送。
像 Claude Code、Cursor 这类成熟 Agent,system prompt 通常长达数千 token,里面包含:
这部分用户平时看不到,但它从根本上塑造了 Agent “看起来很聪明”的行为。换句话说,很多”智能”其实是 prompt 工程的成果,不是模型本身的差异。
把 LLM、System Prompt、工具、消息数组拼起来,AI 编程的核心就是一个不断重复的过程:
用户发请求
↓
┌─→ LLM 思考 ─┐
│ ↓ │
│ 要调工具? │
│ ├─是→ 执行工具 → 结果追加到 messages → 回到循环开头
│ └─否→ 输出文字回复 → 结束
└──────────────┘
写成伪代码就是一个 while:
messages = [system_prompt, user_request]
while True:
response = llm_call(messages) # 1. 调模型
messages.append(response) # 2. 把回复记下来
if not response.tool_calls: # 3. 没有工具调用 = 任务完成
break # 模型的文字回复就是最终答案
for call in response.tool_calls: # 4. 否则执行每个工具调用
result = run_tool(call) # (写文件、跑命令……)
messages.append(tool_result(call.id, result)) # 5. 结果追加回 messages
没有任何神秘机制。所谓”AI 编程”,本质就是这段循环不断调用 API。
关键在第 3 步的判断:模型可以连续多次选择”调用工具”,而不是一次就给最终答案。比如写个 C++ 程序,模型会连续做:
write_file —— 写源码(工具调用)bash: g++ hello.cpp —— 编译(工具调用)bash: ./a.out —— 运行(工具调用)每一步模型都根据上一步工具返回的结果重新决策。这种”看结果再决定下一步”的能力,就是 Agent 和一问一答聊天机器人的根本区别。
循环结束的唯一信号是模型本轮不输出任何 tool_calls。也就是说,“什么时候算做完”也是模型自己判断的——它读了 messages 里所有历史(见消息数组),认为目标已达成,就只输出普通文字,循环跳出。
这也是 Agent 可能”停不下来”或”过早收尾”的根源:判断完全依赖模型对上下文的理解。