ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++构建高性能AI Agent:架构设计与并发实战

C++构建高性能AI Agent:架构设计与并发实战

如果你打开任意一个AI Agent的开源项目,十有八九是Python写的。FastAPI挂上LangChain或者LangGraph,再包一层前端界面,一个Agent demo就这么立起来了。这个组合确实顺手,我也常这么干。可一旦你开始认真琢磨一件事——Agent怎么扛住并发请求、怎么压到个位数的响应延迟、怎么跑在资源受限的端侧设备上,Python这层壳就会第一个成为瓶颈。这时候,C++就会从工具箱里被重新拎出来。

这篇文章是我在"0.1"这个序章位置上写下的第一篇,核心就聊三件事:为什么在2026年这个节点,用C++写AI Agent是一个值得认真考虑的技术选型;一个能落地的C++ Agent整体架构应该长什么样;以及如果你打算从零开始走这条路,阅读路线该怎么规划才不绕远。它适合三类读者:被Python版Agent并发问题卡住、正在做技术选型的后端工程师;想把Agent跑在端侧或实时场景里的嵌入式/游戏开发者;以及单纯想理解Agent底层机制、不想被框架包着黑盒走天下的C++学习者。

1. 先聊透:Python 都这么成熟了,为什么还要用 C++ 折腾 Agent

1.1 AI Agent 到底是什么"体量"的软件

在决定语言之前,得先搞清楚Agent本质上是什么东西。一个Agent不是一个普通的API接口,它是"一个能自主决定下一步调用什么工具、读什么上下文、生成什么回复"的长期运行系统。一个完整的Agent请求,可能包含:用户输入解析、工具检索、多轮模型调用、流式token输出、状态持久化,整个生命周期可能持续十几秒甚至几分钟。

Python在这个场景下的优势非常明显:生态丰富、迭代快、写AI逻辑像写伪代码。但劣势同样明显——GIL锁让多线程形同虚设,CPU密集型任务要靠多进程吃内存,连接数一上去事件循环就开始抖动。更重要的是,当Agent从"demo"走向"生产",它就不再只是"调一次大模型API"那么简单,而是退化成一个高并发的状态机网关:要管理成千上万个会话状态,要并发处理工具调用,要缓冲和转发token流,要面对突发流量而不过载。这个体量的软件,恰恰是C++的舒适区。

1.2 Python 版 Agent 的两个隐形天花板

我做过一个内部的服务型Agent,最初就是用FastAPI + LangChain搭的,上线后遇到两个让我决定重写的问题。第一个是并发连接数。Agent请求不像普通REST请求那样几百毫秒就返回,一个请求挂着SSE流可能要跑一二十秒。Python的异步模型虽然能挂很多"协程",但协程里的同步阻塞操作(比如调工具、查向量库)一旦混进来,事件循环就会被卡住。高峰期CPU没跑满,但大量请求在排队等锁,P95延迟直接翻了三倍。

第二个是推理循环的CPU开销。Agent不是单次调用模型,它是"模型-工具-模型"的循环。每个循环里都要做Prompt模板渲染、JSON解析、工具参数校验、历史记录拼接。这部分在Python里全是一次次的内存分配和字符串拼接,agent的推理步数一多(比如一个复杂任务要循环十几轮),CPU时间就被这些"胶水代码"吃掉了。换成C++之后,同样的逻辑用零拷贝的string_view、对象池、栈上分配,开销能降一个数量级。

1.3 C++ 真正适合的 Agent 长什么样

我必须先把话说明白:C++的Agent不是用来替代Python做快速迭代的,它是用来做规模化承载和极致性能的。我总结了三类真正适合C++的Agent形态:

  • 高并发的Agent网关/中台:统一接收所有Agent请求,做路由、限流、会话管理、工具调度,把请求转发给后端推理服务。这时候Agent本身就是一个高吞吐的C++服务,压在它下面的可能是Python写的工具逻辑或评测系统。
  • 端侧与嵌入式Agent:跑在手机、机器人、边缘盒子上,资源只有几百MB内存。这种情况下用llama.cpp做本地推理,整个Agent循环都在C++里闭环,不依赖外部的Python推理服务。
  • 实时交互型Agent:游戏NPC、交易助手、语音助手,这类场景对延迟极度敏感。C++能保证从音频输入到工具执行再到模型推理的整条链路上,没有垃圾回收的"世界暂停",没有解释器的性能毛刺。

1.4 什么情况下我劝你别用 C++

同样重要的是知道什么时候不该用C++。如果你的Agent还处在业务逻辑快速变化的阶段,Prompt和工具定义一天改八回;如果你是单机自己用,并发量撑死几十路;如果你团队里没人熟悉C++工程化和调试——那还是老老实实用Python(或TypeScript)把业务跑通。C++的价值在"规模"和"确定性",不在"开发速度"。拿开发速度换性能,只有在性能本身成为业务壁垒时才划算。

2. 一个可落地的 C++ Agent 整体架构:五层拆分

2.1 架构总览:从请求进来到响应出去的完整链路

如果把Agent看作一个完整的系统,自上而下我会拆成五层:接入层、编排层、工具层、记忆层、模型网关层。这个分层不是我想当然画的,是实际开发中"请求走一圈"自然形成的。用户消息进来,先到接入层做协议解析和鉴权;然后被编排层接住,编排层决定Agent要思考几步、要不要调工具;一旦要调工具,走工具层去执行具体的动作(查天气、读数据库、调业务API);工具返回结果后,编排层把结果拼进上下文,连同历史记忆一起打包;最后通过模型网关层去请求LLM,拿到token流再原路返回给用户。

这个架构你看着眼熟——它其实跟LangGraph或AutoGen的抽象非常像。区别在于这些框架在Python里是"开箱即用"的库,而在C++里,大部分需要你亲手搭。所以架构设计不只是画框图,更是在划分"自己写"和"用现成库"的边界。

2.2 接入层:网关与长连接管理

接入层处理的是Agent与外部世界的边界。HTTP短请求、WebSocket长连接、SSE流式输出,这三样是Agent服务的标配。C++这边组装方案很成熟:用uWebSockets处理WebSocket和HTTP,它的性能比libwebsockets高一截,接口也干净;流式输出用Server-Sent Events(SSE),格式简单,不用为每一条token都建立WebSocket帧的额外开销。

接入层最容易被低估的是会话生命周期管理。一个Agent会话会存活很久,期间可能有多次请求交互。你不能让每次请求都重新加载一遍历史记忆。我的做法是维护一张会话表:session_id到会话状态的哈希映射,配合智能指针管理生命周期,超时未活动的会话自动回收。这个映射表在C++里用并发哈希表(如libcuckoo或phmap)实现,百万会话也就占几百MB内存,吞吐完全不是问题。

2.3 编排层:Agent 推理循环与状态机

编排层是整个Agent的"大脑",它的核心是那个经典的ReAct循环——推理、行动、观察。C++实现这件事,比Python更有挑战,也更"暴露本质"。因为Python里你可以轻松地写动态代码来模拟循环,而C++你必须很早地定义好:Agent的状态集合是什么?每一步循环里,哪些信息要保留?工具的返回结果怎么反馈给模型?

我的经验是别把编排层做成一个大while循环。否则多轮推理、分支判断、超时中断一旦混在一起,代码很快会失控。更稳的做法是做成显式的状态机:每个Agent请求进来,维护一个状态对象,状态对象里记录了当前是"需要推理"还是"等待工具结果"还是"结束向用户输出"。循环的每一轮,就是一个状态转移。这样好处在于,你很容易加中断、恢复、超时等控制逻辑,还可以把整个状态机序列化下来,做到某种程度上的"Agent暂停和恢复"。

2.4 工具层:函数调用与插件化注册

工具层决定了Agent"能做什么"。在C++里实现工具层,核心是一个注册表:工具名 -> 函数指针/可调用对象。C++17的std::function和std::variant给了我们很好的基础。所有工具对外暴露统一的签名——输入一个JSON对象,返回一个JSON对象。不同业务逻辑做成不同的插件,插件把函数注册进工具表,编排层根据模型返回的function_call信息去查找并调用。

这个设计我强烈建议做类型擦除。就是说,工具注册时,把自己的参数约束齐整:某些参数是string,某些是int,某些是数组。模型返回的JSON参数要按这个约束做校验。没有校验,模型偶尔会给你传非法参数,工具层直接崩——这种诡异问题在Python里顶多抛个异常,在C++里可能直接段错误。所以工具层里做个轻量JSON Schema校验器,非常值得。

2.5 记忆层与模型网关层

记忆层负责管理Agent的上下文。你要存两类东西:一类是长期记忆(用户偏好、知识库向量索引),一类是短期记忆(本会话的历史聊天记录)。短期记忆在C++里用环形缓冲区很合适——窗口大小设成4096个token左右,超出的旧消息自动淘汰,用deque实现即可,O(1)头尾操作。长期记忆建议接向量检索,用hnswlib做ANN索引,配合sqlite-vec做本地持久化,毫秒级召回,不用额外部署一个向量数据库服务。

模型网关层是整个架构里唯一要和外界打交道的层。它做的事情很纯粹:把编排层拼好的Prompt列表,转成符合OpenAI或Anthropic协议的HTTP请求,发出去,再把响应流收回来。C++这边别自己造HTTP客户端轮子,直接用libcurl的multi接口,天然支持异步。还有一点经验:把所有不同的模型供应商封装成一个统一接口,内部用工厂模式创建,这样换模型时业务层完全无感。

3. 扛并发是 C++ Agent 的核心命题:从协程到有界队列

3.1 Agent 的并发模型为什么和普通 Web 服务不一样

普通Web服务处理一个请求平均50毫秒,高并发时只要开200个线程就能扛几千QPS。Agent完全不是这套逻辑——一个Agent请求从进入到返回要10到30秒,期间还不断往外吐token。这种长连接、流式响应、间歇性CPU突发(每次循环里的模型调用间隙是IO等待)的负载特征,直接决定了并发模型的设计。

如果你还按照"一连接一线程"的思路做Agent,那300个并发请求就得开300个线程,调度开销和内存开销都会爆炸。正确的方向是异步事件驱动 + 有限线程池 + 无栈协程。

3.2 C++20 协程:写异步像写同步

C++20的协程是标准库一等公民之后,C++写Agent服务的体验终于不那么拧巴了。协程让异步代码的写法接近同步——发起HTTP请求时挂起,等响应回来时恢复。拿一个Agent循环来说,它在协程里交替执行"等待模型响应"和"执行工具调用",代码逻辑是线性的,但底层的线程不会卡住等待,可以同时服务别的请求。

需要注意,C++20协程是有栈协程的轻量替代,但也带来一个知名的"坑":协程对象的生命周期管理。协程帧(coroutine frame)是堆上分配的,如果忘记处理异常的传播路径,会导致内存泄漏或悬垂引用。我的建议是封装一层RAII的协程任务类(类似TAsTask),用shared_ptr管理协程帧,并显式规定协程只能在某个调度线程上恢复,避免数据竞争。

3.3 有界并发与背压:别让模型网关把你拖垮

模型API不是无限并发的。你给OpenAI同时发2000个请求,它必然限流;你给本地llama.cpp同时发几百个请求,显存先爆。所以在Agent服务内部,必须做有界的并发控制。具体做法是给模型调用层加信号量限流——最大并发模型调用数设个阈值(比如80),超过的请求进有界队列等待。这就是背压:不通过无限排队掩盖过载,而是在入口处快速失败或明确等待。

我踩过一个教训:最开始没做背压,让编排层"能发就发",结果一个活动流量高峰,模型网关层被击穿,所有请求超时重试,重试又叠加新的流量,形成雪崩。后来加了两层保护:一是信号量限流,二是超时熔断。单个模型调用超过20秒直接掐断,连续失败超过10次就给整个Agent服务切降级模式(返回缓存结果或直接报错)。这套在C++里用std::counting_semaphore和atomic计数器,几百行代码就能落地。

3.4 无锁队列与零拷贝:压掉最后几毫秒

除了协程和限流,真正区分工程水平的是细节。Agent循环里,工具返回的数据、模型返回的token、上下文拼接的字符串,每轮要搬运好几次。Python里你只能接受这些开销,C++里可以用手段把它们压掉:

  • 零拷贝字符串:上下文拼接用std::string的append,但如果场景是把多个片段拼成大字符串,预先reserve容量,避免反复realloc。
  • 无锁队列:多个线程同时往响应通道里投递token时,用moodycamel::ConcurrentQueue,吞吐比mutex+queue高好几倍。实测在我的机器上,单线程入队出队能到每秒上千万次,加锁版本也就几百万。
  • 小对象池:每轮循环都会产生临时的JSON对象。用boost::pool或自建内存池复用这些对象,能少很多堆碎片和分配开销。

这些优化做完,Agent服务的CPU占用能降30%到50%,P99延迟能压到原本的一半左右。这在高并发下就是实打实的成本节省和体验提升。

4. 关键依赖选型:场景化清单,不重复造轮子

4.1 推理与模型调用:llama.cpp 还是 OpenAI 协议客户端

先说最核心的选型。你的Agent如果面向端侧或私有部署,唯一严肃的选项就是llama.cpp。它把GGUF格式的量化模型跑在CPU或本地GPU上,还提供了server模式,暴露一个兼容OpenAI的HTTP接口。这样做的好处是——即使底子是C++和llama.cpp,编排层代码依然可以用同一个HTTP协议接口去调用它,和调用云厂商模型保持一致。

如果Agent跑在服务器上、用云厂商模型,那就直接写一个OpenAI协议客户端。关键点是流式处理:HTTP响应是chunked transfer,每个chunk里是一段SSE数据(data: {...})。libcurl的write callback会把chunk一块块送进来,你要做的是用状态机缓存半包、解析整包、提取delta字段,再转发给下层。别小看这个"SSE解析器",写不好就会出现token丢字、响应头错位等问题。

4.2 JSON 与结构化数据:nlohmann/json 的取舍

Agent系统里JSON是无处不在的:各种请求响应、工具参数、模型返回的JSON Mode输出、配置文件。C++这边最主流的库是nlohmann/json,API友好,文档全,适合快速开发;但它有个性能短板——解析和序列化时大量动态分配。

高并发下我建议分场景:I/O密集的边界处(HTTP出入参解析)用simdjson,它是目前最快的JSON解析器,能用SIMD指令一把梭;内部数据结构(Agent状态、配置对象)用nlohmann/json就行。两套JSON混用,要在边界处做一次转换。别嫌麻烦,我在Agent网关里把HTTP入口解析换成simdjson后,解析耗时直接从200微秒降到20微秒,性价比极高。

4.3 通信层:HTTP客户端与服务端

Agent服务通常同时扮演"客户端"和"服务端"两个角色。作为客户端调用模型API时,用libcurl最稳,理由有三:支持HTTP/2(有些供应商不支持HTTP/1.1的长连接会断开导致反复握手)、能开多个并发连接复用、回调机制适合处理流式响应。作为服务端对外提供API时,可以用前面提过的uWebSockets,或者干脆用Drogon——一个完整的C++ Web框架,自带协程支持,路由、中间件、WebSocket都齐了,比手搓HTTP解析靠谱一个量级。

4.4 日志、持久化与监控

  • 日志:spdlog没跑了,用异步logger模式,格式化零成本,排障时才知道什么叫"日志不拖累性能"。重要的一点是给每个Agent请求分配一个request_id贯穿日志,不然排查线上问题时根本拼不出完整链路。
  • 持久化:会话状态、短期记忆、工具调用记录,用SQLite就够了(配合WAL模式读写不互斥)。长期记忆的向量索引,本地用hnswlib,量大再考虑独立的向量库。
  • 监控:Agent服务必须暴露指标——当前活跃会话数、模型调用并发数、工具执行成功率、各阶段的耗时直方图。这也是C++最容易被吐槽"没法监控"的地方。其实用Prometheus的C++客户端库(如prometheus-cpp)就可以暴露标准指标接口,接入Grafana一行配置的事。

4.5 选型原则:核心手写,外围复用

总的来说,C++的Agent生态不像Python那么"全家桶",但核心逻辑(Agent编排、状态管理、并发调度)本来就应该手写,这样你才能真正掌控Agent的每一个决策。外围的东西(HTTP、JSON、日志)尽量复用久经考验的库,别什么都自己造。这条原则能让你既保证核心代码的质量和自由,又不至于在杂事上耗掉太多精力。

5. 0.1 之后的阅读路线:按图索骥,不绕弯子

5.1 路线总览:四阶段,目标明确

写到这里,我按自己走过的路径排一条阅读路线,供你参考。整条路线分四步:先把C++的"硬骨头"补到够用,然后手写一个最小Agent循环,接着做并发与工程化改造,最后找出一个真实场景落地。每一步的产出都得是一个能跑的东西,不要光学不练。

前置要求是至少读过一本C++入门书,知道类、模板、STL容器、引用和指针的区别。如果连这些还模糊,先去补基础——以下路线默认你已经有这个底子。

5.2 第一阶段:把 C++ 的硬骨头补到"够用"

C++的内容浩如烟海,但写Agent服务,真正绕不开的只有几块:内存模型(RAII、智能指针、值语义,为什么要用unique_ptr管理资源)、并发原语(mutex、条件变量、atomic的acquire/release语义)、标准库的字符串和容器(string_view到底怎么做到零拷贝、deque和vector的取舍)。很多初学者卡在C++太久,是因为试图把所有特性学完才动手。不必。你的目标是"够用来写Agent服务",不是"精通模板元编程"。学完基础后直接进入项目,在实战中遇到什么学什么,才是最快的路径。

5.3 第二阶段:手写一个最小 Agent 循环

不要一上来就套框架。用纯C++,写一个最简单的Agent:接收用户输入,拼Prompt,调模型API,拿结果,如果结果里带有工具调用标记,就执行一个写死的工具(比如查当前时间),把工具结果拼回上下文,再调用一次模型,直到模型给出最终的完整回复。

这一步的价值在于你会真正理解Agent的本质——"模型-工具-模型"的循环。你会在代码里具体地感受到:每次循环的上下文是怎么增长并最终逼近窗口上限的;模型返回的JSON参数要如何解析和校验;一个功能完整的Agent循环,核心状态有哪几个字段。这个最小循环跑通后,你再看LangChain或者LangGraph的源码,会觉得豁然开朗——它们不过是把这个循环框架化、工程化了而已。

5.4 第三阶段:并发与工程化改造

最小循环跑通后,加上接入层(HTTP请求进来),你会发现一个请求占一个线程,撑死几十个并发。这时候开始认真做并发改造:把线程模型改成线程池+协程;给模型调用层加信号量限流;给会话状态加并发安全的哈希表管理;把日志、监控指标接进来。这一步是整个路线的核心,也是C++写Agent真正的价值和门槛所在。做完这一步,你会理解为什么我开篇说"Agent的本质是高并发的状态机网关"。

5.5 第四阶段:结合业务场景落地

路线最后一步,是把技术落地到真实场景。我给三个方向供参考:

  • 量化交易助手:Agent负责接收实时行情、检索策略知识、生成交易信号,C++延迟低、能和行情系统无缝对接,天然适合。
  • 端侧个人助理:跑在PC或手机上,用llama.cpp做本地推理,整个Agent闭环都在C++里,离线可用,隐私安全。
  • 游戏NPC系统:Agent驱动NPC决策,每帧都要响应玩家行为,C++和游戏引擎的亲和性无与伦比。

选定场景后,把你前面写的通用Agent框架往场景里套,你会发现很多边界问题(超时策略、降级策略、工具权限管理)需要针对场景单独设计。这些工程细节,才是C++ Agent开发者真正的护城河。


最后说点实在的。我见过不少开发者被"C++难上手"劝退,或者反过来,觉得C++写了Agent就比Python高级、所有场景都要用C++重写。这两种都偏了。C++写Agent,本质是拿开发效率的牺牲,换来可控的延迟、确定的内存表现和高并发下的稳定承载。它适合那些Agent真的被放进生产环境、真的面临大规模真实请求的场景。如果你认准了这条路线,前期耐心把C++的基础补扎实,后面能收获的不仅是"能用C++写AI应用"的成就感,更是一整套对Agent底层机制通透的理解。从0.1这篇开始,后面的路线我会一步步拆开来讲。有问题随时交流,毕竟这摊事,大家也都是边踩坑边往前走的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进