AI 助手的思考引擎:Agent 循环

时间:2026-07-02 09:26:42 来源:互联网

前几篇文章已探讨架构与技术栈,今日则聚焦 mini-cc 的“大脑”——Agent 循环引擎的实战分析。你或许认为循环无非是 while 语句,但生产环境下的实践远非如此简单。核心逻辑虽能抽象为 20 行代码,但真正的挑战在于工程细节的打磨。

Agent 循环到底是什么

简而言之,Agent 循环是感知、思考、行动三阶段不断重复,直至目标达成。

用户输入
│
▼
┌─────────────┐
│感知         │ 接收用户输入,构建上下文
├─────────────┤
│思考         │ 调用 LLM,让它决定下一步
├─────────────┤
│行动         │ 执行工具(读写文件、跑命令)或者直接回复
└─────────────┘
│
│ (有工具调用就继续)
▼
循环……

一句话概括:Agent 持续向大模型询问“接下来如何操作”,执行其指定的动作,再将结果反馈回去,直至模型宣告任务完成。

核心实现

在研究 Claude Code、OpenAI Codex、Cursor 等项目源码后,我发现尽管各框架包装不同,核心逻辑却惊人相似。mini-cc 也不例外,核心代码简化如下:

// src/application/QueryEngine.ts
/** * Agent 循环引擎的核心实现 */
export class QueryEngine {
    private provider: LLMProvider;
    private toolRegistry: ToolRegistry;
    private memory: MemoryManager;
    private maxIterations = 5; // 防无限循环,最多跑5轮

    async run(prompt: string): Promise<AgentResponse> {
        // 1. 构建上下文(从记忆系统里捞历史)
        const context = await this.memory.buildContext(prompt);
        
        // 2. 调用 LLM,看它想干什么
        const response = await this.provider.chat(context);
        
        // 3. 解析 AI 的回复里有没有工具调用
        const toolCalls = this.parseToolCalls(response);
        
        // 4. 如果有工具要执行,执行完把结果喂给 AI,递归继续
        if (toolCalls.length > 0) {
            const results = await this.executeTools(toolCalls);
            return this.run(results); // 递归调用,形成循环
        }
        
        // 5. 没有工具调用了,直接把回答返回给用户
        return response.content;
    }
}

请注意,这一版本与之前文章展示的代码有所不同,主要新增了几项内容:

  1. 最大迭代次数限制:maxIterations = 5。这是实际运行中的教训——测试时因模糊指令导致模型陷入“查资料、看不懂、再查”的死循环,多次运行后才手动终止,因此之后强制设置硬上限。
  2. 更清晰的循环语义:明确区分“有工具调用”和“无工具调用”两条路径,使每一步操作一目了然。
  3. 递归设计:借鉴 ReAct 模式,每次执行工具后携带新上下文递归调用自身,形成自然闭环。虽然也可用 while 循环,但递归写法更直观。

看似简单,真正的挑战并非循环本身,而是围绕循环的工程细节。

系统提示词

要让大模型准确调用工具,必须先告知可用工具及其使用方法。这并非简单的一段描述。

private buildSystemPrompt(): string {
    const tools = this.toolRegistry.list();
    const toolDescriptions = tools.map(tool => {
        return `工具名称: ${tool.name}描述: ${tool.description}参数: ${JSON.stringify(tool.inputSchema.properties)}`;
    }).join('nn');
    return `你是一个专业的 AI 编程助手。可用工具:n${toolDescriptions}n工具调用格式:n<function_calls>n<invoke name="工具名称">n<parameter name="参数名">参数值`;
}

有一个易忽略的点:工具描述的精准度直接影响模型选择。之前有个工具叫 GetCurrentTime,描述仅为“获取时间”,导致用户询问“现在几点了”时,模型误调用其他工具。修改为“获取当前系统时间和时区信息,返回精确的本地时间”后,调用准确率显著提升。

一个真实的多轮交互

仅理论讲解不够直观,以下是一个实际场景。假设对 AI 提出问题:

它会如何应对?

第 1 轮:思考并行动。LLM 收到指令后,判断需要读取文件,返回工具调用:

工具调用: FileReadTool
参数: { "path": "package.json" }

执行工具后,读取文件内容,将结果作为新消息喂给 LLM:

工具结果: {"name": "my-project", "version": "1.0.0", ...}

第 2 轮:继续思考。LLM 拿到结果后发现 name 字段符合预期,无需额外工具,直接返回最终答案,任务完成。

若任务更复杂,例如“给项目添加测试脚本并运行”,模型可能连续多次调用:先通过 FileReadTool 查看当前配置,再用 FileWriteTool 修改,最后用 BashTool 执行测试。整个过程由模型自主决策,轮次越深,工程约束越重要。

不只是 while 循环

最简实现确实只有几十行代码,但要让循环稳定安全运行,必须依赖以下几个“工程补丁”。

1. 防止无限循环:迭代次数上限

if (execContext.iteration >= this.maxIterations) {
    return {
        type: 'error',
        content: `达到最大迭代次数(${this.maxIterations}),已自动终止。`
    };
}

默认设 maxIterations 为 5,大部分任务 3-4 轮结束,超过 5 轮仍循环则可能是死循环或任务模糊,自动中断优于用户等待。

2. 记忆管理

每次循环调用 memory.buildContext(prompt)。mini-cc 采用短期记忆与长期记忆两层结构:

  1. 短期记忆:最近 50 条消息直接拼入上下文。
  2. 长期记忆:超过 50 条时,将历史消息压缩为摘要存储。

实际测试显示,长对话的 Token 消耗减少一半以上,关键信息未丢失。

3. 工具执行超时兜底

AI 调用的 BASH 命令可能耗时几秒或几分钟,必须设置超时保护。BashTool 默认 300 秒超时,文件读写等操作 120 秒。

4. 错误处理

工具执行失败时,将错误信息返回给 LLM,由其判断补救方案,避免循环直接终止。

try {
    const result = await tool.execute(args);
} catch (error) {
    return { type: 'error', content: `执行失败: ${error.message}` };
}

例如,输入错误文件路径时,模型首次调用 read_file 失败后分析错误信息,在下一轮自动修正路径继续执行,全程无需人工干预,体验良好。

两个常见的坑

坑一:忘记递归终止条件

新手最易犯的错误。不设最大迭代次数,模型可能进入“查资料、不够、再查”的死循环而失控。硬上限是第一道防线。

坑二:上下文越堆越大

每轮循环将工具结果直接塞入 messages 而不清理,导致上下文膨胀至数万 Token,既增加 API 费用,又拖慢响应速度。记忆系统并非可有可无,而是刚性需求。

小结

Agent 循环的骨架简单,本质是“询问、执行、再询问”的迭代过程。但使其稳定运行的关键在于外层工程细节:迭代上限、记忆管理、超时兜底、权限控制、错误恢复。mini-cc 所有相关代码集中在 src/application/QueryEngine.ts,可参考源码了解完整实现。

Agent 循环的稳定性依赖于迭代上限、记忆管理、超时兜底等工程补丁,这些细节才是决定系统能否稳健运行的关键。