
1. 为什么不用 Python 而选 C 来写 AI Agent1.1 一个反直觉的选型决定大多数人听到AI Agent这四个字第一反应就是 Python。毕竟 LangChain、AutoGPT、CrewAI 这些主流框架清一色是 Python 写的各种教程、开源项目、论文复现也几乎都围绕 Python 生态展开。所以当我决定用 C 从零搭一个 AI Agent 的时候身边不少人的第一反应是你这不是自找麻烦吗这个疑问非常合理。Python 有现成的 HTTP 客户端、JSON 解析库、异步框架、向量数据库 SDK甚至大模型厂商的官方 SDK 也基本只维护 Python 和 TypeScript 版本。用 C 意味着很多东西要自己造轮子开发周期至少拉长两到三倍。但如果你真的动手写过一段时间 Agent就会发现一个尴尬的事实Python 写的 Agent 在原型阶段很爽一旦要长期运行、要嵌入到已有系统、要做成桌面工具或者边缘设备上的常驻进程问题就全冒出来了。内存占用高、启动慢、依赖管理混乱、打包分发困难、并发模型受 GIL 限制这些都是实打实的痛点。C 恰好能补上这些短板。它不是要取代 Python 在 AI 领域的地位而是针对特定场景提供另一种选择。这篇文章就是要把为什么用 C 写 AI Agent这件事讲透同时给出一个完整的整体架构和阅读路线让你知道从哪下手、按什么顺序推进。1.2 C 写 Agent 真正解决的四个问题先别急着谈架构我们得先搞清楚 C 在这个场景里到底赢在哪。我把实际开发中体会最深的几点列出来。第一是部署与分发。Python Agent 要分发出去要么让用户装 Python 环境要么用 PyInstaller 打包成一个几十上百兆的巨型可执行文件启动还要解压。C 编译出来就是一个静态链接的二进制几兆大小双击就跑扔到任何同架构的机器上都能用。对于要做成桌面助手、CLI 工具、嵌入式常驻服务的场景这个差距是决定性的。第二是资源占用与延迟。Agent 的核心循环是接收输入 → 调用模型 → 解析结果 → 执行工具 → 再调用模型这个循环里真正耗 CPU 的是 JSON 解析、字符串处理、工具调用的调度逻辑。Python 在这些环节的开销不小尤其是高频短请求场景。C 可以把单次循环的额外开销压到微秒级让瓶颈真正落在网络请求上。第三是与现有系统的集成。很多工业软件、游戏引擎、桌面应用本身就是 C 写的。如果 Agent 要用 C 写就能直接以库的形式嵌进去不用搞进程间通信、不用起一个 Python 子进程。热词里出现的ai agent与plc编程就是这个思路的典型——工业控制场景下Agent 需要和底层 C/C 的控制逻辑紧密配合。第四是可控性。C 没有隐藏的运行时魔法内存怎么分配、线程怎么调度、异常怎么传播全在你手里。对于需要长期稳定运行、需要精确控制资源上限的 Agent 来说这种可控性是刚需。当然代价也很明确开发效率低、生态薄、容易出内存和并发 bug。所以我的建议是——如果你的 Agent 只是跑个 demo、做个实验老老实实用 Python如果你要做的是一个要交付、要长期跑、要嵌进别的系统里的产品级 AgentC 值得考虑。1.3 这个系列要带你走完的路线这一篇是整个系列的第 0.1 篇定位是总纲。后面会按模块拆开讲大致路线是这样的第 1 部分项目骨架与构建系统把 CMake、依赖管理、目录结构定下来第 2 部分HTTP 与 JSON 层解决和大模型 API 通信的基础设施第 3 部分Agent 核心循环也就是思考-行动-观察这套调度逻辑第 4 部分工具系统与函数调用让 Agent 能真正干活第 5 部分记忆与上下文管理处理长对话和状态持久化第 6 部分并发与异步把多轮请求和工具执行并行起来第 7 部分打包、测试与部署每一部分都会给出可编译的代码和踩坑记录。你现在不需要记住所有细节只要先建立整体认知知道每一块在整个系统里处于什么位置就够了。2. 一个 C AI Agent 的整体架构长什么样2.1 从输入一句话到输出结果看数据流要理解架构最好的办法是跟着一次完整的请求走一遍。假设用户输入帮我查一下北京今天的天气然后写一首关于天气的短诗。Agent 内部会发生这些事输入层接收用户文本做基本的清洗和编码转换上下文管理器把这条输入和历史对话拼成一个完整的消息列表模型客户端把消息列表序列化成 JSON通过 HTTPS 发给大模型 API响应解析器拿到返回的 JSON判断模型是想直接回答还是想调用工具如果模型要调用工具工具调度器根据函数名找到对应的 C 实现执行它工具的执行结果被塞回消息列表再次发给模型循环往复直到模型给出最终回答输出层把结果返回给用户同时记忆模块把这一轮对话存下来这条链路里每一环都是一个独立的模块模块之间通过明确定义的接口通信。这就是整个架构的骨架。2.2 分层设计把易变的和稳定的隔开我在设计的时候遵循一个原则把容易变的部分和稳定的部分严格分层。大模型 API 的格式、工具的种类、提示词的写法这些都会频繁变动而 HTTP 通信、JSON 解析、线程调度这些底层能力相对稳定。所以架构分成四层层级职责稳定性应用层CLI/桌面/嵌入接口处理用户交互易变编排层Agent 循环、工具调度、上下文管理中等能力层模型客户端、工具实现、记忆存储中等基础层HTTP、JSON、日志、并发原语稳定这样分层的好处是当模型 API 从一家换到另一家你只需要改能力层里的模型客户端编排层和基础层完全不用动。当你想加一个新工具也只需要在能力层加一个实现然后在编排层注册一下。2.3 核心模块清单与职责边界把上面的分层落到具体模块大概是这么几个东西HttpClient封装底层网络库提供同步和异步两种请求方式负责超时、重试、连接池JsonValue一个轻量的 JSON 值类型支持解析、序列化、路径访问ModelClient把消息列表转成 API 请求处理鉴权、流式响应、错误码ToolRegistry工具注册表维护函数名 → 实现的映射生成给模型看的工具描述ContextManager管理消息历史处理截断、摘要、token 预算AgentLoop核心调度循环驱动请求模型 → 执行工具 → 再请求的流程MemoryStore持久化对话和长期记忆可以是文件、SQLite 或向量库每个模块的接口都要设计得足够窄。比如 ToolRegistry 对外只暴露注册工具和按名字调用两个操作内部怎么存、怎么查找是它自己的事。接口窄了模块之间耦合就低测试也好写。2.4 为什么不用现成的 C 框架有人会问不是有 llama.cpp、onnxruntime 这些 C 的 AI 库吗为什么不直接用这里要区分两件事推理框架和Agent 框架。llama.cpp 解决的是怎么在本地跑模型它不解决怎么让模型调用工具、怎么管理多轮对话、怎么调度任务。Agent 框架的核心是编排逻辑不是推理。而且大多数 Agent 场景下模型是通过 API 远程调用的本地根本不需要推理引擎。所以自己搭一套编排层是合理的。当然如果你要做本地推理llama.cpp 可以作为能力层的一个后端接进来这不冲突。3. 阅读路线按什么顺序啃下这套代码3.1 先跑通再理解别一上来就啃源码我给的建议可能和很多教程相反不要从第一行代码开始逐行读。一个完整的 Agent 项目动辄几千行逐行读只会让你在第 200 行就放弃。正确的顺序是先把项目编译起来跑通一个最简单的例子改一改提示词看看输出怎么变加一个自己的工具感受一下注册和调用的流程这时候再回头读核心循环的代码你会发现每一行都能对上号这个顺序的核心逻辑是先建立这个东西能干什么的直觉再去理解它是怎么做到的。反过来做你会被大量和主线无关的细节淹没。3.2 三条主线数据、控制、错误读代码的时候抓住三条主线就不会迷路。数据主线一条用户消息从进入到变成 API 请求中间经过了哪些结构体、哪些转换。这条线走通了你就理解了系统的血液是怎么流的。控制主线Agent 循环是怎么驱动的什么时候调用模型、什么时候执行工具、什么时候结束。这条线走通了你就理解了系统的骨架。错误主线网络超时怎么办、模型返回格式不对怎么办、工具执行抛异常怎么办。这条线走通了你就理解了系统的免疫系统。大部分人读代码只看前两条结果一遇到线上问题就抓瞎。错误处理恰恰是 Agent 这种长链路系统里最考验功力的部分。3.3 每个模块该读到什么深度不是所有模块都值得深挖。我的建议是基础层HTTP、JSON知道接口怎么用就行除非你要换库否则不用读实现能力层模型客户端、工具要读因为这是你日常改动最多的地方编排层Agent 循环、上下文重点读这是整个系统的灵魂应用层看你的具体需求做 CLI 就读 CLI 部分做嵌入就读嵌入部分把精力集中在编排层收益最高。3.4 配套的动手练习光读不练等于没读。每个模块我都建议配一个小练习学完 HTTP 层试着把请求超时从 30 秒改成 5 秒观察行为变化学完工具系统加一个获取当前时间的工具学完上下文管理实现一个只保留最近 10 轮对话的策略学完并发把串行的工具调用改成并行这些练习都不难但能让你真正把知识变成手上的能力。4. 环境准备与第一个可编译骨架4.1 编译器和标准的选择C 标准我建议直接上C20。原因很实际C20 有 concepts、ranges、coroutines、std::format这些在写 Agent 的时候能省很多事。尤其是 coroutines对处理异步请求和流式响应帮助巨大。编译器方面Linux/macOSGCC 11 或 Clang 14WindowsMSVC 2022Visual Studio 2022 自带热词里频繁出现的 vscode配置c/c环境、dev c官网、microsoft visual c redistributable说明很多读者还在纠结工具链。我的建议很直接Windows 上用 Visual Studio 2022 Community 版别折腾 Dev-C。Dev-C 早就停止维护了对 C20 支持很差。VS Code 也可以但要配好 c_cpp_properties.json 和 tasks.json新手容易卡在配置上。至于 microsoft visual c redistributable那是运行时分发的问题开发阶段不用管等打包的时候再说。4.2 依赖管理vcpkg 还是 CMake FetchContentC 的依赖管理一直是个老大难。我的方案是CMake FetchContent理由是这样不依赖外部包管理器clone 下来就能编依赖版本锁定在 CMakeLists 里可复现跨平台一致Windows/Linux/macOS 一套配置vcpkg 也很好但它需要用户先装 vcpkg、配好 toolchain对新手不友好。FetchContent 把依赖源码直接拉下来一起编虽然首次编译慢一点但省心。核心依赖大概这几个nlohmann/jsonJSON 解析头文件库接入简单cpp-httplib轻量 HTTP 客户端/服务端头文件库spdlog日志性能好接口舒服fmt格式化C20 的std::format还没完全普及时的替代如果要用 OpenSSL 做 HTTPS还得加上它这个稍微麻烦点后面单独讲。4.3 目录结构约定一个清晰的目录结构能省掉后面无数麻烦。我用的结构是这样agent/ ├── CMakeLists.txt ├── cmake/ │ └── dependencies.cmake ├── include/agent/ │ ├── core/ │ ├── model/ │ ├── tool/ │ └── util/ ├── src/ │ ├── core/ │ ├── model/ │ ├── tool/ │ └── util/ ├── tests/ ├── examples/ └── third_party/include放公共头文件src放实现tests放单元测试examples放可运行示例。头文件和实现分离方便后面做成库给别人用。4.4 一个最小可编译的 CMakeLists先给一个能跑起来的最小版本后面再逐步加东西cmake_minimum_required(VERSION 3.20) project(cpp_agent LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(cmake/dependencies.cmake) add_library(agent_core src/core/agent_loop.cpp src/model/model_client.cpp src/tool/tool_registry.cpp src/util/json_util.cpp ) target_include_directories(agent_core PUBLIC include) target_link_libraries(agent_core PUBLIC nlohmann_json::nlohmann_json httplib::httplib spdlog::spdlog ) add_executable(agent_cli examples/cli_main.cpp) target_link_libraries(agent_cli PRIVATE agent_core)dependencies.cmake里用 FetchContent 把依赖拉下来。这个文件后面会详细展开。4.5 第一次编译最容易踩的三个坑坑一OpenSSL 找不到。cpp-httplib 要支持 HTTPS 必须链接 OpenSSL。Windows 上建议用 vcpkg 装 openssl或者直接用 MSVC 自带的。Linux 上apt install libssl-dev就行。macOS 用 brew。坑二FetchContent 下载慢。国内网络拉 GitHub 经常超时。可以在 CMake 里配代理或者提前把依赖 clone 到third_party目录用add_subdirectory代替 FetchContent。坑三C20 特性编译器不支持。如果报 concepts 或 coroutines 相关的错先确认编译器版本。GCC 要 11Clang 要 14MSVC 要 19.30。提示第一次编译建议先只编agent_core这个库别急着编可执行文件。库编过了说明依赖没问题再往上加东西。5. 核心循环的设计思路与常见误区5.1 ReAct 循环的本质Agent 的核心循环业界叫得最响的是 ReAct也就是 Reasoning Acting。说白了就是让模型想一步、做一步、看结果、再想。用伪代码表示大概是这样messages [system_prompt, user_input] while not done: response model.chat(messages) if response.has_tool_call(): result execute_tool(response.tool_call) messages.append(response) messages.append(tool_result(result)) else: done true return response.content看起来简单但魔鬼全在细节里。这个循环什么时候该停工具调用失败了怎么办模型一直循环调用同一个工具怎么办这些才是真正要解决的问题。5.2 循环终止条件的三种设计最朴素的终止条件是模型不再调用工具就停。但这不够因为模型可能陷入死循环或者一直返回格式错误的内容。我的做法是三重保险正常终止模型返回纯文本回答没有工具调用轮次上限设置最大循环次数比如 10 轮超过就强制结束并返回当前状态无进展检测如果连续两轮调用了同一个工具、参数也一样判定为卡住主动打断轮次上限这个值怎么定我的经验是 8 到 15 之间。太少了复杂任务做不完太多了浪费 token 还可能跑飞。具体值可以根据任务复杂度动态调整。5.3 工具调用失败的处理策略工具执行失败是常态不是异常。网络会断、文件会不存在、API 会限流。关键是怎么把失败信息反馈给模型让它有机会自我修正。我的策略是把错误当成一种正常的工具结果返回而不是抛异常中断循环。比如工具返回{ status: error, message: 文件 /tmp/data.csv 不存在请检查路径 }模型看到这个下一轮可能会换个路径重试或者告诉用户文件找不到。这比直接崩溃友好得多。但要注意错误信息要写得让模型能理解。别返回一堆堆栈信息模型看不懂。用人话描述问题必要时给出建议。5.4 一个容易忽略的问题上下文膨胀每轮循环都会往 messages 里追加内容几轮下来上下文就爆了。尤其是工具返回的结果可能很长比如读一个大文件。解决办法有几个层次工具结果截断超过一定长度就截断加个内容过长已截断的标记历史摘要把早期的对话压缩成摘要滑动窗口只保留最近 N 轮token 预算管理实时估算 token 数接近上限就触发压缩我一般组合使用工具结果超过 2000 字符就截断历史超过 20 轮就做摘要。具体阈值要看用的模型上下文窗口有多大。5.5 循环里的日志与可观测性Agent 循环是个黑盒出了问题很难查。所以日志必须打全。我一般记录这些每轮循环的开始和结束时间发给模型的完整请求脱敏后模型返回的原始内容工具调用的名字、参数、结果、耗时循环终止的原因这些日志在调试的时候价值巨大。有一次我的 Agent 一直不返回结果看日志才发现是模型每次都在调用同一个工具但参数略有不同触发了无进展检测的边界情况。没有日志根本查不出来。6. 从这篇总纲到后续实战的衔接6.1 先把骨架跑起来再谈优化我知道很多人看完架构就想直接上手写核心循环但我的建议是先把第 4 节那个最小骨架编译通过跑一个hello agent出来。哪怕这个 agent 只会把用户输入原样返回只要它能编译、能运行、能打印日志你就有了一个可以迭代的基础。从零到一最难的不是写代码是搭环境、配依赖、解决编译错误。这些脏活累活先干完后面写业务逻辑就顺了。6.2 后续每篇会给出的东西后面每一篇我都会尽量给出完整可编译的代码不是伪代码关键设计决策的理由不只说怎么做还说为什么这么做实测中遇到的问题和解决办法可以自己动手改的小练习我不打算写成 API 文档那种干巴巴的东西而是像同事之间交流经验一样把踩过的坑、绕过的弯都讲出来。6.3 关于 C 基础的要求有读者可能会担心自己 C 基础不够。我的判断标准是如果你能看懂智能指针、lambda、模板的基本用法能自己用 CMake 编一个多文件项目就够开始了。不需要精通模板元编程不需要懂所有 C20 特性。热词里那些 c八股、c面试、c基础 的内容和实际写 Agent 关系不大。真正需要的是工程能力怎么组织代码、怎么处理错误、怎么调试。这些只能在项目里练看八股是练不出来的。6.4 一个心态上的建议用 C 写 Agent你会经常遇到这个功能 Python 三行就搞定C 要写三十行的情况。这很正常别烦躁。C 的收益不在开发速度在运行时的表现和部署的便利。想清楚你要的是什么就不会在写样板代码的时候怀疑人生。我自己在写这套东西的时候光 JSON 处理就来回改了三版HTTP 客户端也换过一次。这些反复都是正常的。重要的是每一步都让系统更接近你想要的样子。下一篇会从项目骨架和构建系统开始把 CMake、依赖管理、目录结构这些地基打牢。地基打好了后面的楼才盖得稳。