第 1 章

智能体循环

第一章:智能体循环 (The Agent Loop)

使用 LLM 的默认思维模式是请求-响应:输入提示,输出补全,任务完成。

构建智能体需要一种完全不同的模式。

智能体不仅仅是一个更聪明的聊天机器人。它是一种完全不同的架构——在这种架构中,模型不只是回答,而是会采取行动、观察结果并决定下一步该做什么。从「调用 LLM」到「构建智能体」的跨越,就像是从调用一个函数进化到编写一个游戏循环 (Game Loop)。

本章将带你深入了解这个循环。一旦你理解了它,「自主 AI」就会变成一个具体的结构:一个内嵌了语言模型的 while 循环。


请求-响应的局限

你为 LLM 封装了一个漂亮的 API。用户输入:「找出所有导入了 requests 的 Python 文件,检查是否有硬编码的 URL,并修复它们。」你把这段话发给模型,模型回复了……一个计划。也许还有一些伪代码,甚至可能是正确的代码片段。

但实际上什么都没有发生。没有文件被读取,也没有任何东西被修复。

于是你添加了工具——文件读取、编辑、Shell 命令。你把这些工具交给模型。模型返回了一个工具调用 (Tool Call)。太棒了!你执行了它,然后呢?模型已经返回了,函数执行结束了。用户的任务还远未完成。

这就是你撞上的那堵墙。「请求-响应」(Request-Response) 模式无法处理需要多个步骤的任务,尤其是那些模型无法预先判断步骤的复杂任务。

解决方案不是更好的提示词 (Prompting),而是不同的架构。

核心洞察

智能体是一个 while 循环,而不是一次函数调用。

请求-响应模式:

用户:"做 X"
助手:"这是 X"
完成。

智能体循环:

用户:"做 X"
助手:[调用 list_files]
系统:[返回文件列表]
助手:[调用 read_file]
系统:[返回文件内容]  
助手:[调用 edit_file]
系统:[确认编辑成功]
助手:"完成了——我修复了 file2.py 中的一个硬编码 URL。"

对话是持续累积的。每一次迭代都会增加助手的响应和工具的执行结果。不断增长的对话记录 (Transcript) 变成了下一次迭代的上下文 (Context),让模型记住它做过什么,并思考接下来该做什么。

这就是连续模式 (Continuation Pattern):循环执行直到满足终止条件,在迭代之间传递状态,让模型决定何时结束。

基本骨架

这是智能体模式的完整逻辑:

function run_agent(prompt, tools) {
    let messages = [{ role: "user", content: prompt }];
    
    while (true) {
        let response = call_llm(messages, tools);
        
        if (response.has_tool_calls) {
            messages.push(response);
            let tool_results = execute_tools(response.tool_calls);
            messages.push(...tool_results);
        } else {
            return response.text; // 任务完成
        }
    }
}

就是这样。带着完整的历史记录调用模型。如果它请求使用工具,就执行工具,追加结果,然后继续循环。如果它直接回复而不再调用工具,则表示任务结束。

模型自己决定何时继续,何时停止。你不是在编写死板的脚本步骤,而是赋予了它「代理权」(Agency)。

没有历史记录,模型就会陷入「失忆」状态。它不会知道用户的要求是什么,它尝试过什么,哪些失败了,哪些成功了。一个正在调试测试用例的智能体,需要记住在调查新错误之前,它已经修复了之前的导入错误。

消息历史记录不仅仅是日志——它是推理的基石


什么时候停止?

最简单的循环会在模型停止调用工具时终止。但在现实系统中,你需要更多保障:

while (true) {
    if (turns >= max_turns) {
        return error("超出了最大迭代次数");
    }
    if (cost >= budget) {
        return error("预算已耗尽");  
    }
    if (interrupted()) {
        return error("用户中断");
    }
        
    let response = call_llm(messages, tools);
    turns++;
    cost += response.cost;
    
    if (response.error && !is_recoverable(response.error)) {
        return error(response.error);
    }
        
    // ... 处理响应
}

五种终止条件:

  1. 自然完成 (Natural Completion):模型在回复中不再调用工具。这通常意味着任务已完成,不过「我无法完成该任务」也属于自然完成。

  2. 用户中断 (User Interrupt):监控任务的人决定停止。智能体应该始终是可以被中断的。

  3. 最大迭代次数 (Max Turns):防止陷入死循环。简单任务可能需要 5-10 次迭代,复杂项目可能需要数百次。

  4. 预算耗尽 (Budget Exhaustion):API 调用是要花钱的。大型代码库会迅速消耗 Token。设置上限以避免惊人的账单。

  5. 不可恢复的错误 (Unrecoverable Errors):例如身份验证过期、API 宕机等根本性故障。(可恢复的错误——如「文件未找到」——应转化为消息发回给模型。)


像状态机一样思考

将你的智能体视为一个状态机 (State Machine)

任何时刻的状态包括:

每一次迭代都是一次状态转移。当前状态 + 模型响应 = 下一个状态。

function agent_step(state) {
    let response = call_llm(state.messages, state.tools);
    
    if (response.has_tool_calls) {
        let results = execute_tools(response.tool_calls);
        return {
            messages: [...state.messages, response, ...results],
            turns: state.turns + 1,
            cost: state.cost + response.cost,
            done: false
        };
    } else {
        return { ...state, result: response.text, done: true };
    }
}

将这些转移视为不可变的 (Immutable)。不要就地修改状态。为什么?

state = {
    messages: [...],
    session_id: "abc123",
    turns: 7,
    tokens_used: 45000,
    files_modified: ["foo.py", "bar.py"],
    checkpoints: [...]
}

理论上你可以从消息中重构元数据,但实践中这样做既脆弱又昂贵。请明确地记录它们。


流式传输与预执行

现代智能体会逐个 Token 地流式传输响应,以提高响应速度。这创造了一个机会:为什么要等到完整响应结束才执行工具呢?

let stream = call_llm_streaming(messages, tools);
let pending = [];

for await (let chunk of stream) {
    if (chunk.is_text) {
        display(chunk.text);
    }
    if (chunk.is_tool_call_complete) {
        // 立即开始执行
        pending.push(execute_async(chunk.tool_call));
    }
}

let results = await Promise.all(pending);

如果模型请求了三次文件读取,你可以在每个工具调用解析完成后立即并行启动。实际上,并行读取可以将单次迭代延迟减少一半甚至更多。

但并发处理很棘手:

安全的做法:并行运行只读工具,串行运行修改类工具


失败模式

这些问题总会找上门来,请提前做好准备。

无限循环 (Infinite Loops)

无限循环是最常见的故障。模型感到困惑,不断重试一个失败的工具,或者进入死循环:检查状态 → 未就绪 → 等待 → 检查状态 → 未就绪 → 等待……

修复方案:务必强制执行最大迭代次数。必须执行

递归爆炸 (Recursive Explosion)

递归爆炸更加隐蔽:

tools = {
    "ask_assistant": (question) => run_agent(question, tools) // 递归调用
}

智能体生成智能体,智能体再生成智能体。你需要设置每一层的限制,以及全局的总限制。

中途上下文溢出 (Context Overflow Mid-Task)

智能体读取了一个大文件(5万 Token),又读了一个。到了第三步——超出了上下文长度限制。任务完成了一半,却无法继续。

策略:

崩溃导致的状态丢失

工作了二十分钟后,进程崩溃了。如果没有持久化,你必须从头开始。

修复方案:每次状态转移后记录检查点 (Checkpoint)。

while (true) {
    save_checkpoint(state);
    let response = call_llm(state.messages);
    state = apply_transition(state, response);
    save_checkpoint(state);
}

部分执行 (Partial Execution)

模型请求了三次文件写入。第一次成功了,第二次在中途崩溃,第三次根本没运行。系统现在处于不一致状态——重启后,智能体不知道发生了什么。

选项:


心理模型

REPL 模式

// 传统的 REPL (读取-求值-打印循环)
while (true) {
    input = read();        // 获取输入
    result = eval(input);  // 计算结果
    print(result);         // 打印输出
}

// 智能体循环 (Agent Loop)
while (true) {
    response = call_llm(messages); // "读取" 模型想要做什么
    results = execute(response);   // 通过运行工具进行 "求值"
    messages.push(results);        // 将结果 "打印" 到上下文中
}

在 REPL 中,人类决定下一个动作;在智能体循环中,模型决定下一个动作。结构完全一致。

游戏循环 (Game Loop)

while (game_running) {
    process_input();      // 处理输入
    update_game_state();  // 更新游戏状态
    render_frame();       // 渲染帧
}

游戏不会等玩家想好完整计划后才更新。每一帧都在处理输入、更新世界、渲染画面。智能体循环亦是如此——处理上下文、通过工具更新世界、将结果渲染回上下文。

游戏循环还揭示了另一点:每一次迭代都必须足够快。掉帧会导致卡顿,而缓慢的迭代会毁掉用户体验。


调试

当智能体行为异常时,状态机模型就是你最好的朋友。

  1. 转储消息 (Dump Messages):完整的历史记录通常能解释哪里出了问题。它看到了什么导致了错误的决定?

  2. 记录转移过程:当第 47 步失败时,检查第 45、46、47 步的状态,找出是如何发展到那一步的。

  3. 从检查点回放:恢复到较早的状态。修改某些内容,看看结果是否会改变。

  4. 追踪工具影响:仅仅显示「调用了 write_file」提供的信息很少。而「向 src/utils.py 写入了 347 字节,替换了第 12-15 行」则能说明一切。

调试智能体比调试普通代码更难——因为行为是概率性的。但状态机思维为你提供了具体的快照,让你无论模型为什么做出某个行动,都能进行审查。


核心要点

智能体循环看起来简单,实则内核强大:

  1. 它是一个 while 循环。不是简单的请求-响应。循环直到终止。
  2. 消息持续累积。不断增长的对话记录是智能体的「记忆」。
  3. 模型掌握决策权。何时调用工具、何时停止——「代理权」源于循环结构。
  4. 终止需要护栏。最大迭代次数、预算控制、中断处理、错误恢复。
  5. 以状态转移的角度思考。不可变状态使调试、持久化和分支处理成为可能。
  6. 流式传输带来复杂性。预执行工具能提升性能,但需要谨慎处理并发。
  7. 失败是必然的。无限循环、上下文溢出、崩溃、部分执行——为这一切做好设计。

打好这个循环的基础,智能体就站稳了脚跟。其余的一切——工具、提示词、权限、多智能体协调——都是在这个模式之上构建的。