为什么用 C++ 写一个 AI Agent:整体架构与阅读路线
源码仓库:
- 主仓库:gitee.com/chen_dl/ai-…
- A2A 协议库:gitee.com/chen_dl/a2a…
- MCP 扩展平台:gitee.com/chen_dl/mcp…
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
这条链路里有三个关键设计,后面会分别展开:
- 流式事件用结构化对象承载(
AgentEvent):token、工具开始/输出/结束、压缩事件都有明确类型,WebGateway 再转成 SSE 帧推给前端。 - 工具循环没有固定轮次上限。只要模型继续请求工具就继续执行,对齐主流的 agent loop 行为(这是演进后的结果,早期版本有上限)。
- 历史落库与内存缓存同时更新,避免每次请求全表扫描。
4. 核心特性速览
每条都能在源码里找到落点,后续各篇会逐一精读。
| 特性 | 关键实现文件 | 后续篇目 |
|---|---|---|
DeepSeek 同步 / SSE 流式 / tool_calls | src/llm/deepseek_client.cpp | 1.2 |
| Web Gateway(REST + SSE) | src/web/web_gateway.cpp | 1.4 |
| Agent 主循环与结构化事件 | src/agent/agent_runtime.cpp、agent_event.hpp | 1.3 |
| 零依赖 Node BFF 与终端 CLI | web/node_frontend/、cli/ | 1.5 |
| 内置 coding 工具 read/write/edit/bash | src/tools/local_tools.cpp、run_command.cpp | 2.2 |
| MCP stdio 桥接 | src/mcp/mcp_client_bridge.cpp | 2.3 |
| 上下文压缩(token 计量 + 六段摘要) | src/agent/compaction.cpp | 2.4 / 2.5 |
| 会话持久化与内存缓存 | src/agent/history_cache.cpp、session_meta_store.cpp | 2.6 |
| 项目指令 AGENTS.md 加载 | src/agent/project_instructions.cpp | 2.7 |
| 零依赖日志(request id / 脱敏 / 轮转) | src/log.cpp | 2.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.cpp 的 AppOptions。MCP disabled (binary not found) 是因为 vendor/mcp-extension-platform 还没构建:它的 MCP server 是独立进程,构建后重启即可加载。
- 让de agent实现一个坦克大战
- 坦克大战实现效果
7. 本专栏的阅读路线
按依赖关系和难度,专栏分五卷。本文属于卷零(导读),接下来:
卷零 导读 ← 你在这里
卷一 主干:一次对话的全链路
卷二 深水区:Agent Runtime 工程化
卷三 协议:A2A 实现
卷四 扩展:MCP 平台
卷五 收尾:工程方法论与复盘
建议的阅读顺序:
- 想快速理解整体:卷零 → 卷一 → 卷五。
- 对 Agent 内部机制感兴趣:卷零 → 卷二(尤其上下文压缩)。
- 对协议/分布式感兴趣:卷零 → 卷三 → 卷四。
完整选题清单见 99-系列导航.md。
8. 小结
三条要点:
- 写它的目的,是用一个 C++ 项目把 Agent 工程的关键环节完整走一遍:模型调用、工具循环、协议、持久化、可观测性。
- 三个仓库职责分明:主项目做编排,A2A 库定通信,MCP 平台供工具;前者是编译期库依赖,后者是运行期独立进程。
- 阅读入口是
src/main.cpp,它把所有模块连成一张图;这张图就是本专栏后续所有文章的坐标系。
下一篇会把第 6 节的启动过程拆开,讲清每一步在做什么、失败时怎么排查。