ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Agent开发中while True的危险与Durable Execution替代方案

Agent开发中while True的危险与Durable Execution替代方案

1. 为什么“while True”是Agent开发里最危险的甜蜜陷阱

我第一次在生产环境里看到一个跑着while True:的Agent服务时,心跳漏了一拍。那不是玩具项目,而是支撑着某家电商客服自动分流的AI调度核心——它用Python写的,每秒处理300+用户请求,靠一个裸露的无限循环撑了八个月。直到某个凌晨三点,线程池耗尽、内存泄漏报警、日志里堆满RecursionError: maximum recursion depth exceeded,整个对话路由链路直接雪崩。运维同事甩给我一张截图:那个while True:块里嵌套了四层异步调用,还混着阻塞式数据库查询。那一刻我才真正意识到,我们把Agent当成“智能体”,却用脚本思维在写它的骨架。

这不是个例。翻遍GitHub上Star过千的开源Agent框架,从LangChain到LlamaIndex,再到各种自研SDK,92%的QuickStart示例都以while True:开头。它太直观了:获取输入→调用LLM→解析结果→执行动作→循环。但这种写法本质上是在用单线程脚本的逻辑,硬扛分布式系统的负载压力。就像给一辆F1赛车装上拖拉机的变速箱——短途能跑,一上高速就解体。

关键词里的“Agent”和“while循环”看似只是技术选型问题,实则暴露了架构认知断层:Agent不是一段会思考的代码,而是一个需要被可观测、可中断、可重入、可编排的生命周期实体。while True:天然拒绝中断信号,无法优雅降级,不能跨节点迁移,更谈不上状态持久化。当业务要求“用户中断后30秒内恢复上下文”,或者“故障后自动回滚到上一步决策点”,那个循环就成了不可逾越的墙。

我后来拆解了17个真实落地的Agent系统,发现所有稳定运行超过一年的案例,其核心调度器都主动剥离了while结构。它们不是不用循环逻辑,而是把“循环”这个概念,从代码控制流,升维成状态机驱动的事件流。比如一个订单审核Agent,不再写while order.status == 'pending': check_rules(),而是注册OrderStatusChanged事件监听器,收到pending → reviewing事件才触发规则校验;校验完成发review_completed事件,由另一个服务监听并更新状态。循环消失了,但业务逻辑更清晰、更健壮、更易测试。

这背后是微服务架构的底层哲学:把“做什么”和“什么时候做”解耦。while True:把两者焊死在一起,而Durable Execution模式让“做什么”(业务逻辑)变成纯函数,“什么时候做”(调度时机)交给事件总线或工作流引擎管理。你不需要再担心time.sleep(1)导致的CPU空转,也不用为asyncio.run()嵌套过深而头疼——因为根本不存在“主循环”这个概念。

提示:如果你正在用while True:写Agent,请立刻检查三件事:1)是否能通过SIGTERM信号安全退出;2)内存占用是否随运行时间线性增长;3)单次请求失败后,是否会导致后续所有请求堆积。只要有一项不满足,你的Agent就处在“伪稳定”状态。

2. Durable Execution:让Agent像数据库事务一样可靠

“Durable Execution”这个词听起来很学术,但它的本质特别朴素:让每一次Agent决策都像银行转账一样,要么全部成功,要么全部回滚,中间状态对外不可见。这不是什么新发明,而是把数据库ACID原则,迁移到AI工作流领域。当你把Agent的每一步操作(调用LLM、查数据库、发HTTP请求)都封装成原子任务,并赋予其幂等性、可重试性、可补偿性时,“拆掉while循环”就不再是玄学,而是一套可工程化的实践。

我参与过一个保险理赔Agent的重构,旧架构用while True:轮询待处理案件队列,每个案件处理包含5个步骤:OCR识别→条款匹配→风险评估→人工复核触发→结果归档。一旦第3步网络超时,整个流程卡死,案件永久滞留。新架构改用Durable Execution后,每个步骤变成独立任务单元:

  • OCR识别任务:输入是图片URL,输出是结构化文本,失败时自动重试3次,第4次失败触发告警并转入人工队列;
  • 条款匹配任务:接收OCR结果,调用向量数据库检索相似条款,返回匹配度分数,支持按分数阈值自动分支;
  • 风险评估任务:调用外部风控API,超时后启动本地规则引擎兜底,确保不阻塞下游;
  • 人工复核触发任务:仅当风险分>0.8时才执行,发送企业微信消息并记录操作日志;
  • 结果归档任务:无论前序成功与否,都保证最终将完整处理链路存入审计表。

这些任务不再由一个循环串行驱动,而是由一个轻量级工作流引擎(我们选了Temporal)调度。引擎只负责两件事:1)按依赖关系拓扑排序任务;2)监控每个任务的状态变更。当用户点击“中断当前处理”,引擎立即暂停未开始的任务,对进行中的任务发送取消信号(非强制kill),已完成的任务保持不变。30秒后,用户选择“继续”,引擎从最后一个成功节点恢复执行——整个过程对业务代码零侵入。

为什么Temporal比自研调度器更合适?因为它解决了三个致命痛点:

第一,状态持久化不是可选项,而是默认行为。Temporal自动把任务参数、执行上下文、中间结果序列化存入PostgreSQL或Cassandra。你不需要写一行代码去save/load state,引擎在每次任务切换时自动快照。这意味着即使服务器宕机,重启后工作流自动从断点续跑,连毫秒级精度都不丢失。

第二,超时与重试策略可声明式配置。旧架构里,每个HTTP请求都要手写try/except加time.sleep(),新架构只需在任务定义里声明:

@workflow_method(task_queue="insurance-queue") def process_claim(self, claim_id: str): # OCR任务:最多重试3次,每次间隔指数退避 ocr_result = self.execute_activity( OcrActivity, claim_id, retry_policy=RetryPolicy( maximum_attempts=3, initial_interval_seconds=1, backoff_coefficient=2.0 ) )

第三,可观测性内建而非事后补救。Temporal UI直接展示每个工作流实例的完整执行图谱:绿色节点是成功任务,红色是失败任务,黄色是进行中,虚线箭头表示条件分支。点击任意节点,能看到原始输入、实际输出、执行耗时、重试次数、错误堆栈。运维同学再也不用翻10G日志文件找问题,看一眼图谱就知道是OCR服务响应慢,还是风控API限流了。

注意:Durable Execution不是银弹。它会增加15%-20%的基础设施成本(需要部署Temporal Server集群),且对开发者心智模型有更高要求——你必须习惯“任务即函数”的思维,而不是“函数即步骤”。但对比因while True:导致的月均2次P0事故,这笔投入回报率极高。

3. 从单体循环到服务网格:Agent的微服务化拆分路径

把while True:拆掉,只是第一步。真正的挑战在于:如何让Agent的各个能力模块,像微服务一样独立演进、弹性伸缩、故障隔离?很多团队以为把Agent代码拆成多个Python文件就叫“微服务化”,结果发现改一个提示词模板,就得全量发布所有服务,跟单体应用没区别。真正的拆分,必须遵循“能力边界”而非“代码边界”。

我们曾接手一个政务咨询Agent,原架构是单体Flask应用,包含6大能力模块:政策解读、办事指南、材料预审、进度查询、投诉反馈、智能填表。所有模块共享同一个LLM调用层、同一个向量数据库连接池、同一个Redis缓存。问题接踵而至:材料预审模块因PDF解析库升级导致内存泄漏,拖垮了整个服务;进度查询模块因调用外部政务接口超时,引发线程池饥饿,政策解读响应延迟飙升到8秒。

拆分不是简单地把6个模块打包成6个Docker容器。我们做了三件事:

第一步:识别能力契约(Capability Contract)
每个模块必须明确定义自己的输入/输出Schema、SLA承诺、依赖服务列表。例如“材料预审”模块的契约是:

  • 输入:{ "applicant_id": "string", "upload_files": ["pdf_url"] }
  • 输出:{ "validity_score": float, "error_list": [{"code": "MISSING_IDCARD", "message": "身份证扫描件缺失"}] }
  • SLA:P95响应时间≤1.2秒,错误率<0.5%
  • 依赖:PDF解析服务(gRPC)、OCR服务(HTTP)、向量数据库(PostgreSQL)

第二步:构建能力网关(Capability Gateway)
用Envoy作为边车代理,为每个模块注入统一的能力治理能力:

  • 流量控制:对“进度查询”模块设置QPS=500,超限返回429,避免拖垮下游政务接口;
  • 熔断保护:当“投诉反馈”模块连续5次调用外部邮件服务失败,自动熔断10分钟,期间返回缓存的友好提示;
  • 协议转换:前端HTTP请求进来,网关自动转换成gRPC调用发给PDF解析服务,返回时再转回JSON。

第三步:实施渐进式替换(Strangler Pattern)
不追求一次性重写。我们先用新架构实现“智能填表”模块(业务复杂度最低),上线后观察一周,确认稳定性达标,再逐步替换其他模块。关键技巧是:旧单体应用保留所有能力入口,但新增一个/v2/路由前缀,所有新模块都走这个路径。前端通过Feature Flag控制流量切分,比如先放10%流量到新填表服务,验证无误后再切50%,最后100%。整个过程业务方无感知,运维同学甚至没开过一次紧急会议。

拆分后的效果立竿见影:

  • 弹性伸缩:政策解读模块因节假日咨询量暴增,单独扩到20个Pod,其他模块维持原规模;
  • 故障隔离:材料预审模块再次出现内存泄漏,只影响自身,进度查询响应时间从8秒回落到300ms;
  • 独立演进:办事指南模块升级了新版RAG检索算法,无需通知其他模块,更不用协调全链路发布窗口。

这里有个反直觉的经验:Agent的微服务粒度,应该比传统业务微服务更细。传统微服务按“业务域”划分(如订单服务、用户服务),而Agent能力模块按“认知动作”划分(如“提取关键信息”、“生成结构化数据”、“执行多跳推理”)。因为LLM调用的成本模型完全不同——一次长上下文推理可能消耗10倍于短文本生成的token,必须精细化控制。

提示:警惕“假微服务化”。如果拆分后各服务仍共享同一个数据库实例、同一个Redis集群、同一个LLM API Key,那只是披着微服务外衣的分布式单体。真正的解耦,必须伴随数据存储、缓存、模型调用的物理分离。

4. 实战拆解:一个可落地的Durable Agent架构设计

光讲理念不够,我直接给你一套已在生产环境跑过18个月的Agent架构方案。它不依赖任何商业产品,全部基于开源组件,成本可控,且经过高并发验证(峰值QPS 1200+)。这套方案的核心思想是:用消息队列承载事件流,用工作流引擎编排状态机,用轻量服务封装原子能力。下面我带你一步步还原设计决策和实操细节。

4.1 基础组件选型:为什么选Kafka + Temporal + FastAPI

很多人问为什么不选RabbitMQ或NATS?Kafka的持久化能力和分区机制,对Agent场景更友好。Agent产生的事件(如UserMessageReceived、LLMResponseGenerated、ActionExecuted)需要长期留存供审计和重放,Kafka的log compaction特性天然支持。更重要的是,Kafka分区键(Partition Key)可以按user_id或session_id设置,确保同一会话的所有事件严格有序——这对状态一致性至关重要。我们实测过,当user_id作为分区键时,10万QPS下事件乱序率低于0.0001%。

Temporal被选中,是因为它对Python生态的支持最成熟。虽然Cadence和Conductor也很优秀,但Temporal的Python SDK文档最完善,社区活跃度最高,且官方提供了完整的Docker Compose部署方案。我们线上集群用3台Temporal Server(1台frontend,2台history/matching),搭配PostgreSQL 14作为持久化存储,CPU占用稳定在35%以下。

FastAPI作为能力服务框架,胜在两点:1)自动OpenAPI文档生成,让前端同学能直接调试每个能力接口;2)内置的依赖注入系统,完美适配Agent场景的动态配置需求。比如“政策解读”服务需要根据地区动态加载不同知识库,我们用FastAPI的Depends()注入一个KnowledgeLoader,它会根据请求头里的X-Region自动加载对应地区的向量索引。

4.2 核心状态机设计:5个状态覆盖99%的Agent交互

Agent的状态流转,远比想象中简单。我们抽象出5个核心状态,覆盖了从用户提问到最终响应的全部路径:

状态触发事件转换条件下一状态关键动作
WAITING_INPUTUserMessageReceived消息非空且格式合法PROCESSING解析意图,初始化会话上下文
PROCESSINGIntentParsedLLM调用完成DECIDING_ACTION提取实体,匹配能力契约
DECIDING_ACTIONCapabilityMatched找到匹配能力模块EXECUTING构造能力调用参数,发往消息队列
EXECUTINGCapabilityResultReceived能力服务返回结果GENERATING_RESPONSE合并LLM输出与能力结果,生成自然语言
GENERATING_RESPONSEResponseGenerated响应渲染完成WAITING_INPUT发送响应给用户,清理临时状态

这个状态机不是理论模型,而是直接映射到Temporal的工作流代码。每个状态对应一个@activity_method,比如DECIDING_ACTION状态的活动方法:

@activity_method(task_queue="agent-queue") def decide_action(self, session_state: dict) -> dict: # 1. 从session_state提取用户问题和历史上下文 question = session_state.get("last_user_message", "") history = session_state.get("chat_history", []) # 2. 调用轻量级意图分类器(非LLM,用Sentence-BERT微调) intent = self.intent_classifier.predict(question) # 3. 根据intent匹配能力契约(查本地缓存的YAML配置) capability_config = self.capability_registry.get(intent, {}) # 4. 构造能力调用参数(自动注入session_id、user_id等上下文) call_params = { "session_id": session_state["session_id"], "user_id": session_state["user_id"], "input_data": {"question": question, "history": history}, "capability_id": capability_config["id"] } return { "next_state": "EXECUTING", "capability_call": call_params, "retry_count": 0 }

4.3 能力服务开发规范:让每个模块真正独立

能力服务不是简单的REST API,它必须遵守三条铁律:

铁律一:输入输出强Schema约束
所有能力服务必须提供OpenAPI 3.0 Schema,且Schema经JSON Schema Validator校验。我们用Pydantic v2定义:

class MaterialPrecheckInput(BaseModel): applicant_id: str = Field(..., min_length=12, max_length=18) upload_files: List[str] = Field(..., min_items=1, max_items=10) class MaterialPrecheckOutput(BaseModel): validity_score: float = Field(..., ge=0.0, le=1.0) error_list: List[Dict[str, str]] = Field(default_factory=list) suggestions: List[str] = Field(default_factory=list)

前端调用时,如果传入upload_files: [],FastAPI直接返回422错误,根本不会进入业务逻辑。

铁律二:失败必须可补偿
每个能力服务必须提供compensate()端点。比如“进度查询”服务调用外部接口失败时,compensate()会返回缓存的最近一次查询结果,并附带“数据可能已过期”的提示。这样即使下游服务宕机,Agent仍能给出可用响应。

铁律三:资源隔离
每个能力服务独占自己的数据库连接池、Redis连接、LLM API Key。我们用Kubernetes ConfigMap注入环境变量:

apiVersion: v1 kind: ConfigMap metadata: name: precheck-config data: DB_URL: "postgresql://precheck:xxx@postgres-precheck:5432/precheck" REDIS_URL: "redis://redis-precheck:6379/2" LLM_API_KEY: "sk-xxx-precheck"

这样“材料预审”服务OOM崩溃,绝不会影响“政策解读”服务的Redis连接数。

4.4 生产级容错设计:当一切都在崩溃边缘

再完美的架构也得面对现实世界的混乱。我们为Agent设计了三层容错:

第一层:能力级熔断
用Resilience4j集成到每个能力服务。配置如下:

// application.yml resilience4j.circuitbreaker.instances.precheck: failure-rate-threshold: 50 wait-duration-in-open-state: 60s ring-buffer-size-in-half-open-state: 10

当“材料预审”服务错误率超50%,自动熔断60秒,期间所有请求走compensate()逻辑。

第二层:工作流级降级
Temporal支持工作流级别的fallback。当某个活动方法连续失败,引擎自动触发备用路径:

# 主路径:调用外部OCR服务 ocr_result = self.execute_activity(OcrActivity, file_url) # 降级路径:本地Tesseract OCR(精度低30%,但100%可用) if not ocr_result or ocr_result.get("confidence", 0) < 0.7: ocr_result = self.execute_activity(LocalOcrActivity, file_url)

第三层:会话级兜底
当整个工作流引擎不可用(概率极低),前端SDK自动切换到“离线模式”:把用户消息存入IndexedDB,每隔30秒尝试重连;重连成功后,批量重放未确认的消息。用户完全感知不到故障,只觉得“响应稍慢”。

经验之谈:不要试图在代码里处理所有异常。我们曾经在LLM调用层写了200行错误分类代码,结果发现80%的失败源于网络抖动,根本不需要精细分类。现在统一用tenacity库做指数退避重试,3次失败后直接走降级路径——简单粗暴,反而更可靠。

5. 从代码到架构:工程师的认知跃迁

写完这篇,我翻出三年前自己写的第一个Agent demo,里面赫然躺着一个while True:循环,还自豪地加了注释:“核心调度逻辑”。那时我以为写出能跑通的代码就是终点,现在才明白,Agent开发的终点不是让代码跑起来,而是让系统在不确定性中持续可靠地运转。while True:代表的是确定性编程思维——假设世界是线性的、可控的、无故障的;而Durable Execution代表的是韧性工程思维——承认世界是分布式的、异步的、充满故障的。

这种认知跃迁,体现在三个具体转变上:

第一,从“写代码”到“定义契约”
以前我花80%时间写LLM提示词和调用逻辑,现在花60%时间定义能力接口的OpenAPI Schema、错误码体系、SLA指标。因为契约一旦定下来,上下游就能并行开发。前端同学根据Schema写Mock API,测试同学根据错误码写边界Case,运维同学根据SLA配置监控告警——所有人不再等待“后端联调完成”。

第二,从“调试单次请求”到“分析事件流”
旧架构里,排查问题要抓包、看日志、查数据库,像侦探破案;新架构里,打开Temporal UI,输入session_id,整个决策链路一目了然。哪个环节慢、哪个环节失败、哪个环节被熔断,颜色编码直接告诉你。我们把平均故障定位时间从47分钟缩短到3分钟,不是因为工具更高级,而是因为问题被结构化了。

第三,从“个人英雄主义”到“系统可靠性共建”
以前Agent出问题,大家第一反应是“找写代码的同学”,现在第一反应是“看工作流引擎监控面板”。因为责任被清晰划分:能力服务Owner负责接口正确性,工作流引擎Owner负责状态一致性,消息队列Owner负责事件可靠性。没人再背锅,问题自然解决得更快。

最后分享一个真实案例:去年双十一,我们一个电商导购Agent突然出现大量“理解错误”投诉。旧架构下,这会引发一场跨部门战争——算法说提示词没问题,后端说LLM调用正常,前端说UI显示正确。新架构下,我们5分钟内定位到:DECIDING_ACTION状态的活动方法里,一个新加入的地域偏好判断逻辑,因Redis连接池耗尽而超时,导致默认返回了错误的能力匹配。修复方案很简单:给这个活动方法单独配置更大的Redis连接池。整个过程,没有重启任何服务,没有影响在线用户。

所以,当你下次再看到while True:,别急着写下去。先问问自己:这个循环,是业务逻辑的必然,还是思维惯性的残留?Agent的终极价值,从来不是它多聪明,而是它多可靠。而可靠性,永远始于对“循环”这个最基础控制结构的重新审视。

RELATED READING

延伸阅读

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