青雲的博客

Agent 核心就几十行代码,那剩下的几万行到底在解决什么问题?

· 10 分钟阅读

作为一个开源 AI 编程助手 Blade Code 的作者,我来回答这个问题。

先说结论:核心循环确实只有几十行,但让它在真实场景下可靠运行,需要几万行代码来处理「现实世界的复杂性」。


Agent 的核心循环有多简单?

你说得对,Agent 的核心逻辑确实很简单。用伪代码表示大概是这样:

while True:
    response = llm.chat(messages, tools)

    if no_tool_calls(response):
        return response.content  # 任务完成

    for tool_call in response.tool_calls:
        result = execute_tool(tool_call)
        messages.append(tool_result)

Agent 核心循环示意图

sequenceDiagram
    participant Agent
    participant LLM
    participant Tools

    loop Core Loop
        Agent->>LLM: Chat(messages, tools)
        LLM-->>Agent: Response
        
        alt has tool_calls
            Agent->>Tools: Execute Tool
            Tools-->>Agent: Tool Result
            Agent->>Agent: Append Result to Messages
        else no tool_calls
            Agent->>Agent: Return Content
        end
    end

就这么几行。调用 LLM,如果 LLM 想用工具就执行,把结果塞回去继续调用,直到 LLM 觉得任务完成了。

那问题来了:为什么 Claude Code、Cursor、还有我做的 Blade Code,代码量都是几万行起步?


剩下的几万行在解决什么?

我以 Blade Code 为例(7.5 万行 TypeScript),拆解一下这些代码都在干什么。

1. 上下文管理(Context Management)

问题:LLM 有上下文窗口限制,对话长了就会超。

解决方案

  • 对话历史压缩(Compaction)
  • Token 计数和预警
  • 智能截断策略
  • 会话持久化(JSONL 存储)

Blade Code 里有一个 ContextManager,负责管理多层上下文:系统层、会话层、对话层、工具层、工作空间层。每一层都有自己的生命周期和压缩策略。

src/context/
├── ContextManager.ts      # 统一管理所有上下文
├── CompactionService.ts   # 压缩服务
├── TokenCounter.ts        # Token 计数
└── storage/
    ├── JSONLStore.ts      # JSONL 持久化
    ├── MemoryStore.ts     # 内存存储
    └── PersistentStore.ts # 持久化存储

上下文管理架构

graph TD
    CM[ContextManager] --> Layers
    CM --> Storage

    subgraph Layers [Context Layers]
        L1[System Layer]
        L2[Session Layer]
        L3[Conversation Layer]
        L4[Tool Layer]
        L5[Workspace Layer]
    end

    subgraph Storage [Storage & Processing]
        S1[MemoryStore]
        S2[PersistentStore JSONL]
        S3[CacheStore]
        P1[ContextCompressor]
        P2[ContextFilter]
    end

你提到的 uuid/parentUuid 线程机制,就是用来追踪对话链的。当需要压缩时,可以根据这个链条决定哪些消息可以合并、哪些必须保留。

2. 文件修改的历史快照(Snapshot)

问题:AI 改错文件怎么办?用户想回滚怎么办?

解决方案:每次文件修改前自动创建快照。

// src/tools/builtin/file/SnapshotManager.ts
export class SnapshotManager {
  async createSnapshot(filePath: string, messageId: string): Promise<SnapshotMetadata> {
    // 生成文件哈希
    const fileHash = this.generateFileHash(filePath, version)
    // 保存到 ~/.blade/file-history/{sessionId}/{fileHash}@v{version}
    await fs.writeFile(snapshotPath, content)
    // ...
  }

  async restoreSnapshot(filePath: string, messageId: string): Promise<void> {
    // 恢复到指定版本
  }
}

这不是什么高深的技术,但没有它,用户就没有「后悔药」。

3. 工具执行管道(Execution Pipeline)

问题:工具执行不是简单的「调用函数」,需要权限检查、用户确认、错误处理、结果格式化……

解决方案:7 阶段执行管道。

Discovery → Permission → Hook(Pre) → Confirmation → Execution → PostHook → Formatting

每个阶段都有自己的职责:

  • Discovery:找到对应的工具定义
  • Permission:检查权限(allow/ask/deny 列表)
  • Hook(Pre):执行前置钩子(用户自定义逻辑)
  • Confirmation:危险操作需要用户确认
  • Execution:实际执行工具
  • PostHook:执行后置钩子
  • Formatting:格式化结果给 LLM 和用户

工具执行管道流程

flowchart LR
    A[Discovery] --> B[Permission]
    B --> C[Hook Pre]
    C --> D{Confirmation}
    D -- Approved --> E[Execution]
    D -- Denied --> End[Abort]
    E --> F[PostHook]
    F --> G[Formatting]
    G --> Result[Result]

为什么要这么复杂?因为你不能让 AI 随便 rm -rf /

4. 子代理(Subagent)

问题:复杂任务需要分解,主 Agent 不能什么都干。

解决方案:支持子代理,每个子代理有自己的工具集和上下文。

src/agent/subagents/
├── SubagentExecutor.ts    # 子代理执行器
├── SubagentRegistry.ts    # 子代理注册表
├── AgentSessionStore.ts   # 会话隔离
└── BackgroundAgentManager.ts # 后台任务管理

比如「搜索代码库」这种任务,可以交给专门的搜索子代理,它只有只读工具,不会误改文件。

5. 权限和安全

问题:AI 能执行 Shell 命令,能读写文件,能访问网络。这太危险了。

解决方案:多层安全机制。

  • 权限模式:default / autoEdit / plan / yolo
  • 工具白名单/黑名单
  • 敏感文件检测(.env、私钥等)
  • 路径安全检查(防止路径穿越)
  • 文件锁(防止并发写入冲突)
// src/tools/validation/SensitiveFileDetector.ts
const SENSITIVE_PATTERNS = [
  /\.env$/,
  /\.pem$/,
  /id_rsa$/,
  /\.ssh\/config$/,
  // ...
]

6. 多模型适配

问题:不同 LLM 的 API 格式不一样,流式响应处理方式不一样,Tool Calling 格式也不一样。

解决方案:统一的 ChatService 接口 + 多个实现。

src/services/
├── ChatServiceInterface.ts   # 统一接口
├── AnthropicChatService.ts   # Anthropic 实现
├── OpenAIChatService.ts      # OpenAI 实现
├── GeminiChatService.ts      # Google 实现
├── AzureOpenAIChatService.ts # Azure 实现
└── ...

每个实现都要处理:

  • 流式响应的增量解析
  • Tool Calling 的参数累积(流式时参数是分块的)
  • 错误重试和降级
  • Token 使用量统计

7. 用户界面

问题:终端里怎么显示 Markdown?怎么显示代码高亮?怎么显示 diff?

解决方案:React + Ink 的终端 UI。

src/ui/
├── components/
│   ├── MessageRenderer.tsx    # 消息渲染
│   ├── CodeHighlighter.tsx    # 代码高亮
│   ├── DiffRenderer.tsx       # Diff 显示
│   ├── ThinkingBlock.tsx      # 思考过程显示
│   └── ...
└── utils/
    ├── markdown.ts            # Markdown 解析
    └── markdownIncremental.ts # 增量 Markdown 渲染

流式响应时,Markdown 是一点一点来的,你不能等全部接收完再渲染,要增量渲染。这比想象中复杂得多。

8. MCP 和插件系统

问题:内置工具不够用,用户想扩展怎么办?

解决方案:支持 MCP(Model Context Protocol)和插件系统。

src/mcp/
├── McpClient.ts       # MCP 客户端
├── McpRegistry.ts     # MCP 服务注册
├── HealthMonitor.ts   # 健康检查
└── auth/              # OAuth 认证

MCP 让 Agent 可以连接外部工具服务器,比如数据库、API、甚至其他 AI 服务。

9. 还有一堆「看不见」的东西

  • 日志系统:调试时需要知道每一步发生了什么
  • 配置管理:全局配置、项目配置、运行时配置的合并
  • 版本检查:提醒用户更新
  • 错误边界:某个组件崩了不能影响整体
  • 优雅退出:Ctrl+C 时要清理资源
  • 测试:单元测试、集成测试、E2E 测试

一个类比

Agent 核心循环就像汽车的发动机原理:「吸气-压缩-点火-排气」,四个步骤,小学生都能理解。

但真正造一辆能上路的汽车,你需要:

  • 燃油系统(上下文管理)
  • 刹车系统(权限控制)
  • 安全气囊(错误处理)
  • 仪表盘(用户界面)
  • 变速箱(多模型适配)
  • 导航系统(任务规划)
  • 保养记录(日志和快照)

发动机原理是核心,但只有发动机,你哪儿也去不了。


最后

如果你对这些实现细节感兴趣,可以看看 Blade Code 的源码。它是开源的,代码结构还算清晰。

或者直接用起来:

npm install -g blade-code
blade

你会发现,当你真正用一个 Agent 来完成实际工作时,那些「额外的几万行代码」带来的体验差异是巨大的。


参考资料

  1. Blade Code 源码:https://github.com/echoVic/blade-code
  2. Claude Code 系统提示逆向分析(多个社区讨论)
  3. MCP 协议规范:https://modelcontextprotocol.io/

相关文章

评论