0.1-为什么用 C++ 写一个 AI Agent:整体架构与阅读路线

0 阅读8分钟

为什么用 C++ 写一个 AI Agent:整体架构与阅读路线

源码仓库:

ai-agent-cpp 是一个用 C++20 写成的 AI Agent,把「DeepSeek 模型调用」「工具调用循环」「A2A 协议对外服务」「MCP 工具扩展」四件事组装成一个可运行的服务。它由三个仓库协作完成:主项目做编排,A2A 库定通信,MCP 平台供工具。

这篇是专栏开篇,不深入任何模块,只做一件事:画出一张完整的地图——三个仓库怎么分工、一个请求怎么流动、源码该从哪里开始读。后面每一篇都会沿这张地图往下钻。

适合谁读:会 C++、调过 ChatGPT 或 DeepSeek 接口、但没亲手搭过 Agent 的工程师。需要能读懂 C++20(模板、RAII、线程)和基本的 HTTP/JSON;A2A、MCP 这些协议不用预先了解,遇到时会解释。

至于为什么不用现成框架、为什么选 C++,下一节展开。


1. 问题背景:为什么又要手写一个 Agent

市面上的 Agent 框架已经很多了。再手写一个,我先说清楚它解决的是什么问题,以及代价是什么。

1.1 一个具体的运行场景

设想我要的是一个本地终端 coding agent

  • 在终端输入一句话,它流式输出;
  • 需要时它能读文件、改文件、跑 bash
  • 能调用外部工具(MCP 插件);
  • 能作为 A2A 服务被其它 Agent 调用;
  • 长会话不能撞上下文窗口;
  • 关掉终端再打开,会话还在。

这些需求单独看都不难,难的是同时成立且可控。用现成框架通常能快速搭起来,但一旦要改协议细节、改上下文策略、改工具执行方式,就会不断和框架的抽象层对抗。

1.2 为什么是 C++

最主要的原因很朴素:我日常工作接触最多的就是 C++ 。用熟悉的语言,精力可以放在 Agent 本身的机制上,而不用将精力分给一门新语言。

在熟悉的基础上,C++20 也确实适合做这件事:

  • 单进程、单二进制、无运行时依赖:不需要 Python 虚拟环境,也不需要 Node 服务做胶水(Web 前端是可选附加)。
  • 对底层协议有完全控制:A2A 的 JSON-RPC 绑定、SSE 分帧、MCP 的 stdio 读写,都是自己实现的,出问题能直接定位。
  • 性能与并发可控:多会话并发、工具执行、日志轮转都在一个进程内,用线程和锁管理,不依赖事件循环的黑盒。
  • 作为学习载体:协议、网络、并发、持久化、编译构建,一个项目全都能覆盖。

代价也很明确:开发速度慢于脚本语言,字符串、JSON、HTTP 都得自己拼装或引依赖。所以这个项目的做法是只在不值得自己写的地方引依赖:JSON 用单头文件 nlohmann/json,HTTP 用 cpp-httplib(header-only),SQLite 用官方 amalgamation,其余全部手写。

我没说 C++ 比别的语言更适合写 Agent。用 Python、Go、TypeScript 一样能写,很多时候还更快。选 C++ 更多是「我熟悉 + 想顺便练手」,谈不上技术选型上的胜负。


2. 三个仓库的分工

这三个仓库按三个方向拆开:做应用定协议供能力

仓库作用源码量级(约)
ai-agent-cpp应用层,把模型、工具、协议、前端组装成一个可运行的产品C++ 6.5k 行 + Node 4k 行
a2a-protocol实现 A2A v1.0,负责 Agent 之间的互相调用C++ 5k 行
mcp-extension-platform实现 MCP,负责工具的注册、发现与执行C++ 5k 行 + Python 插件

三者之间是依赖关系:

 ai-agent-cpp
   ├── 依赖 a2a-protocol   → 对外暴露 A2A 服务(HTTP/JSON-RPC/SSE)
   └── 通过 stdio 启动 mcp-extension-platform → 获得一堆可调用的工具
  • a2a-protocol 通过 Git submodule 引入(vendor/a2a-protocol),在 CMake 里作为子项目直接编译;
  • mcp-extension-platform独立的服务进程ai-agent-cpp 用 stdio 与之通信,不把它编进自己的二进制。

这个区别很重要:协议库是编译期依赖,工具平台是运行期进程。前者可以看成"一个库",后者是一个要被启动的"外部系统"。


3. 一个请求的一生

这是整个专栏的主线。用户在浏览器或终端说一句话,它经历的完整路径如下。

3.1 组件架构

flowchart TB
    subgraph client[客户端]
        Browser[浏览器 Web UI]
        CLI[终端 CLI de]
    end

    subgraph app[ai-agent-cpp 进程]
        BFF[Node BFF<br/>web/node_frontend]
        GW[WebGateway<br/>REST + SSE]
        A2A[A2A AgentHttpServer<br/>JSON-RPC]
        RT[AgentRuntime<br/>历史 + 工具循环]
        DS[DeepSeekClient]
        TOOLS[CompositeToolProvider]
        LOCAL[LocalCodingTools<br/>read/write/edit/bash]
        BRIDGE[MCPClientBridge]
        CACHE[HistoryCache]
        META[SessionMetaStore]
        STORE[(SqliteTaskStore)]
        INST[ProjectInstructionsLoader<br/>AGENTS.md]
        LOG[Logger]
    end

    subgraph ext[外部]
        API[DeepSeek API]
        MCPSRV[mcp-extension-platform<br/>stdio JSON-RPC]
    end

    Browser --> BFF --> GW
    CLI --> GW
    GW --> RT
    A2A --> RT
    RT --> DS --> API
    RT --> TOOLS
    TOOLS --> LOCAL
    TOOLS --> BRIDGE --> MCPSRV
    RT --> INST
    RT --> CACHE
    GW --> META
    GW --> STORE
    RT --> STORE
    GW -.事件.-> Browser

3.2 时序:一次带工具调用的流式对话

sequenceDiagram
    participant U as 用户(CLI/浏览器)
    participant G as WebGateway
    participant R as AgentRuntime
    participant D as DeepSeekClient
    participant T as ToolProvider
    participant S as SQLite

    U->>G: POST /api/v1/chat/stream (message, session_id, cwd)
    G->>R: HandleMessageStreamingEvents(...)
    R->>R: 加载历史 + 注入 AGENTS.md system
    loop 直到模型不再请求工具
        R->>D: ChatStream(messages, tools)
        D-->>R: SSE token / tool_calls
        R-->>G: token 事件
        G-->>U: SSE 逐 token 推送
        opt 模型发起 tool_calls
            R->>T: Call(tool_name, args)
            T-->>R: 工具结果
            R-->>G: tool_start / tool_output / tool_end
        end
    end
    R->>S: Put(task.history)
    G-->>U: done

这条链路里有三个关键设计,后面会分别展开:

  1. 流式事件用结构化对象承载AgentEvent):token、工具开始/输出/结束、压缩事件都有明确类型,WebGateway 再转成 SSE 帧推给前端。
  2. 工具循环没有固定轮次上限。只要模型继续请求工具就继续执行,对齐主流的 agent loop 行为(这是演进后的结果,早期版本有上限)。
  3. 历史落库与内存缓存同时更新,避免每次请求全表扫描。

4. 核心特性速览

每条都能在源码里找到落点,后续各篇会逐一精读。

特性关键实现文件后续篇目
DeepSeek 同步 / SSE 流式 / tool_callssrc/llm/deepseek_client.cpp1.2
Web Gateway(REST + SSE)src/web/web_gateway.cpp1.4
Agent 主循环与结构化事件src/agent/agent_runtime.cppagent_event.hpp1.3
零依赖 Node BFF 与终端 CLIweb/node_frontend/cli/1.5
内置 coding 工具 read/write/edit/bashsrc/tools/local_tools.cpprun_command.cpp2.2
MCP stdio 桥接src/mcp/mcp_client_bridge.cpp2.3
上下文压缩(token 计量 + 六段摘要)src/agent/compaction.cpp2.4 / 2.5
会话持久化与内存缓存src/agent/history_cache.cppsession_meta_store.cpp2.6
项目指令 AGENTS.md 加载src/agent/project_instructions.cpp2.7
零依赖日志(request id / 脱敏 / 轮转)src/log.cpp2.8
A2A v1.0 协议实现vendor/a2a-protocol/3.x
MCP 平台(传输 / 插件 / RAG)vendor/mcp-extension-platform/4.x

5. 源码导航:从哪开始读

5.1 主仓库目录

 ai-agent-cpp/
 ├── CMakeLists.txt              # 顶层构建:5 个静态库 + 主程序 + 测试
 ├── conf/
 │   ├── app.ini.example         # 配置模板([deepseek]/[mcp]/[logging]/[compaction]/[instructions])
 │   └── conf.ini                # 本地密钥配置(gitignore)
 ├── include/aiagent/            # 公共头文件
 │   ├── agent/                  # AgentRuntime / compaction / history_cache / instructions / session_meta
 │   ├── llm/                    # DeepSeek 客户端与类型
 │   ├── mcp/                    # MCP stdio 桥
 │   ├── tools/                  # 工具抽象与内置实现
 │   └── web/                    # Web Gateway
 ├── src/                        # 与 include/ 对应的实现 + main.cpp
 ├── tests/                      # C++ 测试(11 个 ctest 用例)
 ├── examples/                   # deepseek_chat 命令行示例
 ├── cli/                        # 终端 coding 客户端(Node,零依赖)
 ├── web/node_frontend/          # Node BFF + 静态前端
 ├── de                          # 一键启动器(守护后端 + CLI)
 ├── scripts/                    # install/uninstall de
 └── vendor/                     # 两个 submodule

CMake 里的目标划分很清晰,按依赖从底到顶:

 aiagent_log    ← 日志,零依赖
 aiagent_llm    ← 配置 + DeepSeek 客户端(依赖 a2a)
 aiagent_agent  ← AgentRuntime / 工具 / 压缩 / 缓存 / 指令(依赖 llm)
 aiagent_mcp    ← MCP 桥(依赖 llm)
 aiagent_web    ← Web Gateway(依赖 agent)
 aiagent_app    ← 主程序,组装以上全部

5.2 两个子仓库

  • vendor/a2a-protocol/core/(JSON-RPC、错误码、SSE、HTTP)→ models/(Part/Message/Task/AgentCard)→ server/(AgentServer、TaskManager、TaskStore、HTTP 服务端)→ client/(客户端与 Agent Card 发现)。
  • vendor/mcp-extension-platform/src/server/(JSON-RPC 路由)→ src/transport/(Stdio/SSE/HttpStream)→ src/loader/(.so 与 .py 插件加载)→ src/bridge/(Python 子进程桥)→ src/rag/(语义检索)。

5.3 阅读入口

如果你想自己读代码,建议顺序是:

 src/main.cpp
   → include/aiagent/agent/agent_runtime.hpp
   → src/agent/agent_runtime.cpp
   → src/llm/deepseek_client.cpp
   → src/tools/local_tools.cpp
   → src/mcp/mcp_client_bridge.cpp
   → src/web/web_gateway.cpp

main.cpp 是整个项目的"组装说明书",所有模块在这里被 new 出来并互相连接。读懂了它,就知道每个模块的边界在哪。


6. 先跑起来

不深入细节,先把服务拉起来,确认环境没问题。

 # 1. 拉取两个 submodule
 git submodule update --init --recursive
 ​
 # 2. 构建(配置阶段会自动下载 cpp-httplib 与 SQLite)
 cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
 cmake --build build -j
 ​
 # 3. 跑测试:不需要真实 API Key
 ctest --test-dir build --output-on-failure

实测输出(本机 GCC 11.4 / CMake 3.22):

 100% tests passed, 0 tests failed out of 11

这三个步骤不需要任何 API Key:测试用本地 mock HTTP server 和 fake MCP 进程完成验证。只有当你要做真实对话时,才需要:

 cp conf/app.ini.example conf/conf.ini   # 填入真实的 DeepSeek api_key
 ./build/aiagent_app --config conf/conf.ini --db build/db/aiagent_tasks.db

启动后的输出(本机实测,模型名以 conf.ini 为准):

 MCP disabled (binary not found)
 ai-agent-cpp listening on http://127.0.0.1:18080/
 Web Gateway: http://127.0.0.1:18081/
 Agent Card: http://127.0.0.1:18080/.well-known/agent-card.json
 Task DB: build/db/aiagent_tasks.db
 Press Ctrl+C to stop.

A2A 在 18080、Web Gateway 在 18081,默认端口与 db 路径来自 src/main.cppAppOptionsMCP disabled (binary not found) 是因为 vendor/mcp-extension-platform 还没构建:它的 MCP server 是独立进程,构建后重启即可加载。

  • 让de agent实现一个坦克大战

0.1-1 让de agent实现一个坦克大战.png

  • 坦克大战实现效果

0.1-2 坦克大战实现效果.png


7. 本专栏的阅读路线

按依赖关系和难度,专栏分五卷。本文属于卷零(导读),接下来:

 卷零  导读            ← 你在这里
 卷一  主干:一次对话的全链路
 卷二  深水区:Agent Runtime 工程化
 卷三  协议:A2A 实现
 卷四  扩展:MCP 平台
 卷五  收尾:工程方法论与复盘

建议的阅读顺序:

  • 想快速理解整体:卷零 → 卷一 → 卷五。
  • 对 Agent 内部机制感兴趣:卷零 → 卷二(尤其上下文压缩)。
  • 对协议/分布式感兴趣:卷零 → 卷三 → 卷四。

完整选题清单见 99-系列导航.md


8. 小结

三条要点:

  1. 写它的目的,是用一个 C++ 项目把 Agent 工程的关键环节完整走一遍:模型调用、工具循环、协议、持久化、可观测性。
  2. 三个仓库职责分明:主项目做编排,A2A 库定通信,MCP 平台供工具;前者是编译期库依赖,后者是运行期独立进程。
  3. 阅读入口是 src/main.cpp,它把所有模块连成一张图;这张图就是本专栏后续所有文章的坐标系。

下一篇会把第 6 节的启动过程拆开,讲清每一步在做什么、失败时怎么排查。