1. 为什么想不开要用 C 写 AI Agent先把结论摆在最前面用 C 写 AI Agent不是因为 C 时髦恰恰相反是因为它在某些场景下“没得选”。我最初动这个念头是在做一个需要常驻在边缘设备上的本地智能体时——设备内存只有几百兆CPU 是 ARM 架构没有 Python 运行时也没有 Node。你让我在这种环境里跑一个动辄几百兆依赖的解释型 Agent 框架根本不现实。于是只能回到最底层用 C 从零把 Agent 的骨架搭起来。这个系列我打算完整记录一遍一个能跑、能扩展、能接大模型、能调用工具的 AI Agent怎么用 C 从 0 到 1 搭出来。第一篇不写代码先把整体架构和阅读路线讲清楚。因为如果一上来就贴代码你大概率会在“为什么这里要抽象一层”“为什么不用现成库”这些问题上卡住最后抄了个四不像。先明确一下这个 Agent 到底要干什么。它不是一个聊天玩具而是一个具备以下能力的常驻程序接收用户输入把输入和上下文组装成提示词发给大模型解析模型返回的“动作意图”调用本地工具读写文件、执行计算、访问数据库等把工具结果再喂回模型循环直到任务完成。这套“思考—行动—观察”的循环就是 Agent 和普通聊天机器人的本质区别。那为什么是 C我总结了三个真实理由。第一是部署环境受限嵌入式、桌面客户端、游戏引擎插件、工业软件这些地方 Python 生态很难塞进去。第二是性能与延迟敏感Agent 的循环里涉及大量字符串拼接、JSON 解析、网络往返C 能把单次循环的开销压到很低尤其是高频调用工具的场景。第三是与现有 C 代码库集成很多工业软件、CAD、游戏本身就是 C 写的Agent 要“下地干活”最自然的方式就是直接嵌进去而不是再起一个 Python 进程做 IPC。当然代价也很明显。C 没有现成的 LangChain没有一行代码接大模型的便利JSON 要自己选库HTTP 要自己选库异步要自己设计。所以这个系列的价值就在于把这些“别人帮你做好的事”一件件用 C 重新做一遍并且讲清楚每一步的取舍。适合谁看有 C 基础、想搞明白 Agent 底层原理、或者需要在受限环境里落地智能体的人。如果你只是想快速搭个 Demo那 Python 确实更省事这篇你可以当架构科普看。2. 整体架构设计一个 Agent 到底由哪些部件组成2.1 从“思考—行动—观察”循环拆出五大模块不管什么语言写的 Agent核心循环都是一样的。我用最朴素的话描述一遍用户给一个目标模型想一步决定要不要调工具调完拿到结果再想下一步直到它认为任务完成输出最终答案。这个循环在 C 里落地我把它拆成五个模块每个模块职责单一方便单独测试和替换。第一个是输入输出层负责和外界打交道可以是命令行、可以是 socket、也可以是嵌进宿主程序的回调。第二个是上下文管理模块维护对话历史、系统提示词、工具描述还要处理上下文长度超限时的裁剪策略。第三个是模型客户端封装对外的 HTTP 请求负责把上下文序列化成模型能懂的格式再把返回解析回来。第四个是工具注册与调度模块维护一张“工具名到函数”的表负责参数校验和实际调用。第五个是循环控制器也就是 Agent 的大脑决定什么时候继续循环、什么时候终止、出错怎么重试。这五个模块里最容易被低估的是上下文管理。很多人以为 Agent 就是把历史消息一股脑塞给模型实际上一旦对话轮次多了token 会爆成本会飙模型还会“忘事”。所以裁剪策略、摘要策略、关键信息提取这些都得在 C 里自己实现。我后面会专门用一篇讲这个。2.2 为什么采用“接口抽象 具体实现”的分层C 写 Agent 最大的坑是把所有逻辑写在一个巨大的 main 函数里。我第一版就是这么干的结果换一个模型厂商改代码改到怀疑人生。后来我改成面向接口的分层设计核心是三个抽象基类ILLMClient、ITool、IContextStore。循环控制器只依赖这三个接口不依赖任何具体实现。这么设计的好处很直接。换模型厂商写一个新的ILLMClient实现就行控制器一行不用动。加一个新工具继承ITool注册进去即可。想把上下文从内存换成 SQLite 持久化实现一个新的IContextStore。这就是所谓的“依赖倒置”在 C 里通过纯虚函数和智能指针实现成本很低收益极高。有人会问抽象会不会带来性能损失虚函数调用确实有一次间接寻址但在 Agent 这个场景里真正的瓶颈是网络往返和模型推理一次虚函数调用的开销完全可以忽略。所以这里不要过早优化先把结构做对。2.3 单线程还是多线程并发模型的取舍热词里有个“ai agent 怎么扛并发”这问题很实在。我的建议是Agent 的单个会话循环用单线程多个会话之间用线程池。为什么单个会话不并行因为 Agent 的循环本质是串行依赖的——下一步的输入依赖上一步的输出你没法把“思考”和“行动”真正并行化强行并行只会引入复杂的同步问题。但多个用户、多个会话之间是可以并行的。我的做法是维护一个会话表每个会话绑定一个独立的上下文和状态机用一个固定大小的线程池去调度。这里要注意模型客户端和工具注册表是无状态的可以被多线程共享而上下文是有状态的必须每个会话独占。这个边界划清楚并发就不会乱。还有一个细节HTTP 请求本身是阻塞的如果线程池里每个线程都卡在网络等待上吞吐会很低。所以我在模型客户端里用了异步 IO或者退一步用“一个请求一个线程 连接池”的简单方案。具体选哪种取决于你的 QPS 目标后面实操篇会给出两种方案的对比。3. 核心技术点拆解C 写 Agent 绕不开的几块硬骨头3.1 JSON 处理选 nlohmann 还是 RapidJSONAgent 和大模型之间几乎全靠 JSON 通信所以 JSON 库的选择是第一个决策点。我实测下来nlohmann/json上手最快语法像 Pythonjson::parse和j.dump()两行搞定适合快速开发和原型验证。但它的缺点是内存占用偏高解析大响应时性能一般。RapidJSON则是另一个极端性能极强内存可控但 API 偏底层写起来啰嗦容易出错。我的选择是开发阶段用 nlohmann性能瓶颈出现后再局部替换成 RapidJSON。因为 Agent 的 JSON 报文通常不大几 KB 到几十 KBnlohmann 完全够用没必要一上来就上重型武器。这里有个坑要提醒大模型返回的 JSON 经常不“干净”可能带 markdown 代码块标记可能字段缺失可能类型不对。所以解析前一定要做容错预处理比如剥掉 json 包裹、补全缺失字段、对类型做宽松转换。这个逻辑我封装成了一个safe_parse函数后面会给完整实现。3.2 HTTP 客户端libcurl 是绕不过去的选择C 标准库没有 HTTP所以必须选第三方库。libcurl是最成熟、跨平台最好的选择几乎所有系统都有预编译包。它的用法是经典的 C 风格回调写起来不算优雅但胜在稳定。另一个选择是cpp-httplib纯头文件用法简单适合轻量场景但它对 HTTPS 和复杂超时的支持不如 libcurl。我的方案是用 libcurl 做底层传输外面包一层 C 的 RAII 封装把curl_easy_init和curl_easy_cleanup放进构造函数和析构函数避免忘记释放。同时封装一个post_json方法输入 URL、请求头、请求体输出响应字符串把超时、重试、错误码处理都收进去。这样上层调用就一行干净利落。要注意的是libcurl 的全局初始化curl_global_init必须在任何线程创建之前调用而且整个进程只调一次。这个坑我踩过多线程下没初始化就发请求会随机崩溃排查了很久。3.3 工具调用的参数校验与安全边界Agent 调用工具是最危险的地方因为模型可能生成任意参数。比如一个“删除文件”工具模型可能传进来一个../../etc/passwd这样的路径。所以工具层必须做参数校验和权限边界不能信任模型。我的做法是每个工具在注册时声明自己的参数 schema包括类型、范围、是否必填。调用前先按 schema 校验不通过就直接返回错误给模型让它重新生成。对于涉及文件系统、命令执行的工具还要额外做路径白名单和命令白名单。这不是过度设计而是 Agent 落地的底线。另外工具的执行要有超时控制。模型可能让你执行一个死循环的计算如果不设超时整个 Agent 就卡死了。我用的是“独立线程执行 超时后放弃等待”的方案虽然不能真正杀死线程但至少能让主循环继续跑。3.4 上下文裁剪token 预算怎么算上下文裁剪是 Agent 能不能长期运行的关键。我的策略是给上下文设一个 token 预算比如 8000 token然后按优先级填充系统提示词永远保留最近 N 轮对话优先保留更早的对话做摘要压缩工具返回的超长结果做截断。token 的估算不需要精确用“字符数除以 3”这种粗略公式就够了因为目的是控制量级不是精确计费。真正要精确的时候再调用模型厂商的 tokenizer。裁剪的触发时机是在每次请求模型之前而不是每次追加消息之后这样能减少不必要的计算。这里有个经验摘要压缩要用模型来做而不是简单截断。简单截断会丢失关键信息导致模型“失忆”。我一般保留最近 5 轮完整对话更早的用一次模型调用生成摘要摘要控制在 200 字以内。这样既省 token又保留了语义。4. 阅读路线这个系列我会怎么带你走4.1 从环境搭建到第一个能跑的循环阅读路线我按“能跑起来”到“能扛住”再到“能扩展”三个阶段来组织。第一阶段是环境搭建和最小可运行版本。你会看到怎么在 VS Code 里配好 C 环境怎么用 CMake 管理依赖怎么把 libcurl 和 nlohmann 拉进来。然后写一个最简单的循环发一条消息给模型拿到回复打印出来。这一步不涉及工具目的是先把“请求—响应”这条链路打通。这个阶段最容易卡在环境上。热词里“vscode配置c/c环境”“microsoft visual c redistributable”这些搜索量很高说明很多人第一步就受阻。我会把 Windows 和 Linux 两套配置都写清楚包括编译器选择、CMake 版本、依赖安装命令。Windows 上还要注意运行库的问题缺vcruntime会导致程序起不来这个我会单独提醒。4.2 加入工具调用让 Agent 真正“下地干活”第二阶段是让 Agent 具备行动能力。我会实现工具注册表、参数校验、调用调度然后接入几个实用工具读文件、写文件、执行计算、查询时间。到这一步你的 Agent 就能完成“帮我算一下这个目录下所有文件的总大小”这类任务了。这个阶段的核心是循环控制逻辑模型返回的是普通文本还是工具调用请求怎么区分怎么把工具结果拼回上下文怎么判断任务结束这些状态机的细节我会用一张状态转移表讲清楚避免你写出死循环。4.3 并发、持久化与工程化收尾第三阶段是工程化。包括多会话并发、上下文持久化到 SQLite、日志系统、配置管理、错误重试。到这一步你的 Agent 才算是一个能上生产的东西而不是玩具。我会重点讲并发模型的设计和线程安全的边界这是 C 写 Agent 最容易翻车的地方。最后还会有一篇讲扩展怎么接入不同的模型厂商怎么支持流式输出怎么把 Agent 嵌进现有的 C 项目。这些内容会随着系列推进逐步展开每一篇都保证是可编译、可运行的完整代码而不是伪代码。5. 实操心得与常见问题速查5.1 我踩过的几个典型坑第一个坑是字符串编码。大模型返回的 UTF-8 文本里可能包含多字节字符如果你用std::string::size()去算长度做截断很可能把一个汉字截成半个导致乱码甚至解析失败。正确做法是按 UTF-8 码点遍历或者直接用成熟的 UTF-8 库处理。这个坑我在做上下文裁剪时踩得很惨截断后的 JSON 直接解析报错。第二个坑是异常安全。C 里网络请求、JSON 解析、文件操作都可能抛异常如果不在合适的地方捕获一个请求出错就可能让整个 Agent 进程挂掉。我的做法是在循环控制器的每一轮迭代外面包一层 try-catch把异常转成错误消息喂回模型让模型自己决定怎么处理。这样 Agent 就有了“自愈”能力。第三个坑是依赖版本冲突。libcurl 和 OpenSSL 的版本如果对不上会出现链接错误或者运行时崩溃。建议用包管理器vcpkg 或 conan统一管理依赖版本不要手动下载编译。我早期手动编译 OpenSSL折腾了一整天最后发现是版本不匹配。5.2 常见问题速查表问题现象可能原因排查方向程序启动即崩溃缺少运行库安装对应版本的 Visual C Redistributable请求返回乱码编码不一致确认请求和响应都用 UTF-8检查 Content-Type多线程下随机崩溃全局资源未初始化检查 curl_global_init 是否在创建线程前调用JSON 解析失败模型返回带 markdown 包裹解析前剥离代码块标记做容错处理Agent 陷入死循环终止条件判断有误检查工具调用次数上限和任务完成判定逻辑上下文超长报错未做 token 裁剪实现裁剪策略设置 token 预算工具调用参数错误未做 schema 校验注册工具时声明参数类型调用前校验5.3 给准备入坑的人几句实在话如果你 C 基础还不牢比如指针、引用、值传递的区别还搞不清智能指针用得不熟那我建议先补一补再来看这个系列。Agent 的代码里大量用到std::shared_ptr、std::unique_ptr、lambda、std::function这些是基本功。热词里“c 引用 指针 和 值传递”搜索量高说明这是很多人的薄弱点值得花时间搞明白。另外不要一上来就追求“大而全”。我见过太多人想一步到位做一个支持几十种工具、多模型、分布式的 Agent结果卡在第一个 HTTP 请求上就放弃了。正确的姿势是先跑通最小闭环再逐步加功能。这个系列也是按这个节奏来的每一篇只解决一个问题稳扎稳打。最后说一句关于选型的个人体会C 写 Agent 确实比 Python 累但累的地方恰恰是你能真正掌控的地方。当你需要把 Agent 塞进一个没有 Python 的环境或者需要把延迟压到毫秒级或者需要和现有 C 系统深度集成时这些“累”就变成了优势。工具没有好坏只有合不合适。
阅读完成 · 觉得有帮助?