一、Agent 已经能帮我们干很多活了
今天形形色色的 Agent 产品,已经超脱了原来纯 chat 的范式。我们和 AI 聊天探讨某个方案,方案确认的时候,可以让 AI 顺便把活干了:编个代码,搭个组件,做个 demo,生成一份 PPT,做一次数据整理。个人助手类的产品,还可以订个机票,买个东西,记个日志,发个邮件,等等。
我们始终以 chat 的模式,完成和大模型的交互。这个过程中,人讲清任务,给定目标,验收结果。一轮轮的对话是对过程的干预,也是对结果的纠正。某种程度上,这是人深度干预的工作流。
今天的 Agent 除了能干活,它的推理能力和长程任务规划能力,在很多 3~5 步的工作上已经可以做得很稳健,人需要干预的颗粒度正在越变越粗。大模型训练的数据飞轮,也在持续强化它对常见长任务的规划能力。我们日常工作和生活中的琐碎事务,它已经能解决得很好。
二、还有一类任务:生产级的长任务
还有一类任务,我把它叫做**"生产级的长任务"**。它们有几个特点:
- 链条很长(细拆下来往往有几十个步骤)
- 确定性要求很高(高准确率、可追溯、可审计)
- 过程低人为干预,甚至无人为干预(最后只审结果,结果要易审核,只需小改)
拿招投标领域的招标文件解析来说,从文档的提取、格式的转化和清洗、内容的分块,到义务要求的提取、归类、归并、关联、风险排查等等,每个环节下都有多个细分步骤,还有并发分支。一份文件解析下来,可能涉及上百次,甚至大几百次 LLM 的并发调用。
三、为什么不能交给 Agent 自主规划
这样的生产级任务,目前还没有办法通过 Agent 自主规划来实现。
不得不承认,今天的 Agent 产品已经能自主规划多个步骤。拿 Claude 来说,丢给它一份招标文件,它会自主规划:把 Word 内容提取、分块,甚至会做主题分类,进行义务要求的提取、归纳总结,最后输出报告。我看过那个报告,做得相当可以。但在实际使用的过程中,你并不清楚哪里可能出现了幻觉,哪里漏了条款。总的来说,查错成本高。粗放的解读它做得很好,但要精细化的拆解,还是不够。
而且因为任务比较长,大模型每次自主规划都会有一些不同,所以它产出的质量是漂的。
再者,Agent 自主规划、纠错等一系列循环处理的动作,是非常耗费 token 的。也就是说,我们可能在让它重复去干一件本来就很确定的事情。对于确定的流程,你不需要每次让它规划,也不需要让它重新写代码来做数据清洗、上下文工程、提示词 harness 等工作。
确定性的东西本来就应该沉淀下来,这才是效率最高的做法,也是质量严控的做法。
还有一点,很多生产级的流程,在市面上是没有通用的对错标准的。它不像编程,代码能不能跑、测试过不过,有明确的判定;业务上的判定规则来自企业自身。每次让 Agent 临时生成一套提示词方案,没有经过 harness 和 evals 的测试,这套提示很可能漏洞百出。
工作流里每一个环节任务拆解的颗粒度、数据输出的结构,都特别影响大模型单点的生产质量。总的来说,生产级的长任务工作流,需要手工编排,需要工程体系的保障,涉及每一个节点精细化的设计和测试。
四、这篇文章讲什么
这篇技术贴里,我们不讲任务拆解、上下文管理、提示词工程,以及 harness、token 优化等部分。
我们来谈谈架构保障。要让一个长达几十分钟、涉及上百次 LLM 调用的工作流跑起来,碰到网络或 API 不稳的情况,还能稳稳接住,工作流不坏死,可接续,并不是一件特别容易的事情。
五、整体技术栈
我们先来看看一个全栈 AI 工作流产品的架构。下面的讨论以这套技术栈为基础,这是我实战下来感受最好的一套。
text
前端:React
后端:Python FastAPI
存储:Redis、PostgreSQL
后台任务:arq
工作流:状态机主干 + DAG(节点内部)
自建 LLM 底座:模型切换、熔断、格式验证、全局 QPM 限流和并发控制、任务追踪
六、弃用的 LangChain
大模型配套的工程技术发展很快。最初接触 LangChain,是两年前开始搭工作流的时候,我被它优雅的 chain 式写法所吸引。但后来越来越痛恨它黑匣子一样的封装:作者的工程范式和我的偏好有差异,每次要给 LLM 添加工程保障的时候,都绕不开 LangChain 自身的底层架构。
于是我改成了自己封装、一层层构建的方式:从 base_layer 开始,一层层地叠加和封装。一切尽在掌控中!
今天的 LangChain,我相信已经很成熟了,这么久没碰,它一定有了更多更成熟的设计。只是我始终认为,在 AI coding 这么成熟的今天,抛弃那些被别人固化的框架,自己造轮子的成本并不高。依照自己的思路构建,代码的掌控性和可控性都更好。
七、自己搭建 LLM 底座
一层层搭上去以后,代码量不少,要测试的细节也多,但其实逻辑并不复杂。
1. 模型选型与接入
最底层自然是选模型:一个主模型,加一到两个备用。
我曾经搞了五六个不同厂商的备用模型,觉得很酷,后来发现都用不上。而且到了提示词 harness 的环节,模型往往要固化下来。比如用了 DeepSeek 的模型,就一直用它,不要再切其他模型。因为不同模型的训练数据、训练过程、架构都有差异,这在 evals 的测试过程中暴露无遗:相同的提示词跑 20 遍,好不容易在一个模型上调稳了,换一个模型,结果差异就很大。
所以基本就是一款模型,几个备用的云服务商。我用的是 DeepSeek,主模型走官方,备用走其他云厂商部署的 DeepSeek。由于云厂商不同,接口有差异,usage 参数、缓存命中的方式也不一样,所以需要做模型适配。
第一层,基本上就是在解决模型选型和接入的问题。
2. 错误分类与切换时机
一款模型不稳,什么时候切,是有讲究的,所以需要区分不同的错误类型。
比如 429 过载,我们基本上是要杜绝的,不能让它出现,这要在更上层的队列里做保障。
再比如有一种错误很恶心:API 已经连接,但迟迟没有返回,错误无法触发,只能硬生生等任务超时,一等往往好几分钟。这是前期踩过的一个坑。
这也是为什么,不管是否需要流式展示,我都建议走流式连接,然后自己再加一层封装。流式的反馈更及时,我们可以更细颗粒度地监控连接建立、首个 chunk 返回、chunk 间隔这些信号,设置更短的超时,从而更早发现厂商 API 的异常。
3. 格式验证
在长工作流里,我们基本要的是 JSON 格式。所以可以开启 JSON Object 模式,这是大模型接口自带的 JSON 强制输出。
但这还不够,我们可以再加一层 schema validation。这一层只管格式:格式对不对,字段缺不缺,字段类型对不对。**不对就立即重跑,不必升到上层。**多次未解决,再做软处理,或者直接报错。
一些格式清洗也顺手在这一层做掉,不要留到上层,比如 ```````json```` 这类 Markdown 标识。
4. 资源平衡
再上一层,就要处理资源平衡了。
由于需要全局资源平衡,模型实例要全局共用一个。我的产品比较特殊,有项目级、用户级,还有全局级,所以需要设计多层 semaphore。这里涉及三件事:全局容量保护、业务公平性、供应商 API 保护。
另外因为采用全局保护,所以需要用锁来避免竞态。各个瓶颈的设置也有讲究,先触发哪一个。比如 QPM 检查应该前置,快速失败。
5. 任务管理
再往上是任务管理:每个 LLM 调用都作为一个 task 被记录,用于全局监控。
6. 多任务并发与缓存命中
有了以上几层,单个 LLM 调用的底座就完成了。再往上做多任务并发,然后是断点续传。最终,一套可以在各个地方搬运套用的 LLM 底座就完成了。
拿并发来说,实测 DeepSeek 需要先跑完两个任务,缓存命中才会体现出来。所以设计的时候,是先串行跑两个任务,再并发剩余任务。与之配套,提示词里的输入占位要放到最后,让前面的部分成为可复用的前缀。
八、自建状态机工作流和 DAG
如果说 LLM 底座是工作流的智能节点,那么状态机工作流就是把这些节点串起来的工程保障。
一套相对完整的状态机工作流,包括以下内容:
- 状态和全局 context 设计(状态数据、过程的日志数据、任务数据、阶段结果数据)
- 两级存储:Redis + PostgreSQL
- 写入的竞态保护(数据什么时候写,每次加锁)
- arq 的 task 衔接(长任务进后台)
- 并发任务的断点续传
- 工作日志的发出(前端才知道任务到了哪里)
- 人工干预:结果数据的修改、重试、重跑、暂停
- 针对前端的 snapshot 设计
- 分析环节的 DAG 设计
一套完整的工作流体系还是挺复杂的。从 0 到 1 逐步构建,即便有 AI coding 的辅助,边学边搭,边测试,到代码能跑,再到把中间竞态的坑 debug 干净,实战下来也要几个月,才能有一套稳稳的、可复用的体系。
下面这个是一个状态机实例的结构图。
下面挑几个关键的设计展开聊。
1. 断点续传:存储要分开,颗粒度要细到 LLM 调用
context 和日志的存储,与断点续传的存储,需要分开。因为它们的写入频率不同,而且走的是 arq,放在一个存储里,基本无法避免竞态覆盖。前期我在这个问题上踩了很多坑,调试测试浪费了很多时间。
断点续传有两层。 状态机本身是一层,但这一层不够。因为我们的业务往往某个步骤下有多个子步骤,子步骤下存在 LLM 并发,经常是大几十的并发量,甚至更多。如果每次都在状态层接续,Step 内部就得重跑,这个颗粒度过大,每次重跑时 LLM 的重新分析都是浪费。
所以最佳的断点续传颗粒度,是在单次 LLM 调用上做,而这需要独立设计存储。
2. 业务代码和状态机要解耦
业务在开发的时候,经常会添加或者删除步骤。特别是大模型任务,我们可能发现 evals 怎么也无法收敛,而根源是几个任务放在一起,存在任务间的张力拉扯。这时可能需要拆分步骤,或者对待分析的 context 提前做结构或语义上的处理。如果耦合构建,就会出现牵一发动全身的问题。
解耦设计后,状态机可以做成配置模式,添加和删除环节都通过配置来实现,而业务节点则通过 task 接入。
3. 数据分层与前后端通信
之前说到,这里有两类数据:
- 工作流数据:包含 context、日志、子任务完成情况
- 结果数据
这两块要分开处理。第一种数据和状态机绑定,第二种数据和业务数据深度耦合,这本身也体现了解耦设计的思想。同样,context 和阶段结果数据也要分开:阶段数据往往很大,而 context 更多记录的是工作流的状态和 snapshot 的信息。
说到通信模式,这里采用 HTTP 和 SSE。SSE 发消息,不要把 context 放进去,只发通知。 通知在前端按类型对应不同的触发,触发 HTTP 去更新数据。所以是 SSE 驱动 HTTP 更新数据,再进行渲染。渲染走 snapshot,snapshot 在后端生产。所以前端的渲染是由后端的数据驱动的。
Agentic UI 有一个典型的思考模块或任务模块,展示的是大模型在思考什么,或者后端 agent 在做什么;在工作流的场景里,就是展示当前在哪个步骤。这类工作日志类的消息走 SSE,在前端和 context 拼接,到阶段节点时再与后端对账。否则这类消息更新很多,每次都拉 snapshot,会影响性能。
不管是阶段的结果数据,还是 snapshot,都可以走增量更新,这样有助于提升 HTTP 的传输性能。
对于工作流,前端的展示有两种很通用的范式:stepper 模式和 chat 模式。chat 模式很像今天流行的 agent UI,我们也可以设计成引导式的对话流。它们的数据本质上是一样的,前端针对不同组件做数据的 adapter 处理就好了。(前端配套组件后面单独展开聊)
4. DAG:分析环节按数据依赖推进
我们完全可以不采用 DAG,而直接走状态机。本来这也不是问题,但灵活性不足。
状态机是受状态推动往前走的,而 DAG 关注的是数据依赖:某一份数据 ready,就可以启动依赖它的其他步骤。这对分析环节很适用,因为某个分析能否开始,要看它要分析的数据是否 ready。这样一来,分析多一个维度,完全没有必要多一个状态步骤,只需要在 DAG 里多一条边就好。
DAG 和状态机通过接口对接,本质上是两套东西,DAG 有自己完整的存储体系。
总的来说,状态机的设计有很多细节,以上只是说了几个令我印象深刻、踩坑比较多的点,实际构建和测试的过程中,还有更多值得关注的地方。但一旦形成了自建体系,它就是一套通用的工程配套,可以用于大部分生产级的长任务工作流。
九、实际效果
这样设计以后,实际跑下来,很稳。
一份 60 页、8 万多字的招标文件,跑下来共 532 次大模型调用。过程中出现了 9 次格式异常,全部完成了自我修复。
当然,不同的招标文件,由于上下文的差异,有些文件出现的异常会比另一些多。也有格式异常超出重试次数的情况,最后走软修复的方式。
对于我做的这款无需人为过程干预的产品来说,任务能跑完,比中途中断垮掉,对用户更好。过程中做了软修复的地方,根据不同情况,有的会写进 check flag,作为用户重点核查的点。