ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

生产级Agent执行引擎:Agent Loop与运行器设计实战

生产级Agent执行引擎:Agent Loop与运行器设计实战 1. 这不是考概念是考你有没有真跑过 Agent —— 从一次差点翻脸的面试说起“一个 Agent 收到任务以后是怎么一步步执行的如果工具一直超时模型不停重试怎么办”这句话我去年在三场技术面试里都听过最后一次面试官刚问完我就下意识把椅子往前挪了十公分手已经搭在键盘上想打开本地调试终端——不是要争辩是真想现场 pull 出来跑一遍给你看。因为这个问题根本不是在问 LLM 的 prompt 工程也不是考你背没背过 ReAct 或 Plan-Execute-Observe 的论文标题它是在问你有没有亲手写过一个能在线上稳定跑满 72 小时、自动处理 200 并发请求、在数据库连接断开后不卡死、在天气 API 返回空数组时不无限循环的 Agent有没有在凌晨两点盯着日志看着agent execution terminated due to error.这行红字一边改重试策略一边骂自己为什么没加熔断核心关键词就五个Agent、Agent Loop、工具调用、任务状态、运行器。它们不是并列关系而是有严格因果链的执行骨架。Agent是角色Agent Loop是心跳工具调用是手脚任务状态是神经反馈运行器是操作系统内核。缺一不可错一个环节整个流程就从智能体退化成“AI 焦虑症患者”——反复读指令、反复调工具、反复失败、反复 retry最后内存爆掉、线程锁死、日志刷屏。适合谁看不是给刚学完 LangChain 文档的新人看的“Hello World Agent”而是给已经用过langgraph写过带条件分支的 workflow、用crewai搭过双 agent 协作、甚至自己封装过ToolExecutor的人看的实战复盘。如果你还在纠结“Agent 和 Skill 有什么区别”建议先去跑通一个带 timeout 控制的真实工具链如果你已经能用harness做 benchmark但线上部署后发现pi agent在高并发下任务堆积如山那这篇就是为你写的。它不讲定义只讲你代码里漏掉的那行if state.get(retry_count, 0) 3: raise TaskFailedError()。我做过 4 个生产级 Agent 项目一个对接银行核心系统的对账机器人要求 99.99% 可用性、一个嵌入 CRM 的销售线索自动打标服务每天处理 12 万条 lead、一个医疗知识库问答网关需通过 HIPAA 合规审计、还有一个工业设备故障预测调度器与 OPC UA 实时通信。所有项目上线前都被 QA 用“模拟工具超时 随机网络抖动 强制 kill 进程”三连击测了至少 17 轮。这篇内容就是从这 17 轮压测、327 份错误日志、以及 5 次凌晨紧急回滚中熬出来的血泪笔记。2. Agent Loop 不是循环是带状态机的精密流水线2.1 别再画“思考→行动→观察→反思”的四步圆环了几乎所有入门教程都在画这个环LLM 输出 action → 调用 tool → 获取 observation → 再喂给 LLM。这图看着优雅实操起来就是灾难源头。真实系统里这个“环”根本不是数学意义上的闭环而是一条带分支、带闸门、带快车道、带维修通道的工业级流水线。它的每一步都必须有明确的状态出口、错误捕获点和人工干预接口。我见过太多团队因为迷信这个“完美环”结果在生产环境里出现agent execution terminated due to error.后整个任务直接消失连 trace ID 都找不到。真正的 Agent Loop 必须拆解为6 个原子阶段且每个阶段都强制绑定状态字段Task Ingestion任务注入接收原始输入HTTP body / Kafka message / WebSocket frame生成唯一task_id初始化state {task_id: ..., created_at: ..., status: pending, retry_count: 0, max_retries: 3}Plan Generation计划生成LLM 输出结构化 plan不是自由文本必须是 JSON Schema 校验过的{ steps: [{tool: search_db, args: {...}}, ...] }写入state[plan]Step Dispatch步骤分发运行器根据state[current_step_index]从 plan 中取 step校验 tool 是否启用、参数是否合法、配额是否充足Tool Execution工具执行真正调用外部 API/DB/CLI必须包裹三层防护超时控制非全局 timeout而是 per-tool、熔断器基于失败率动态开关、降级 fallback如天气 API 失败时返回缓存数据Observation Handling观测处理不是简单把 response.body 塞回去。要解析 HTTP status、检查 schema、提取关键字段、标记is_success/is_partial/is_timeout并决定下一步是continue、retry还是abortState Commit Loop Control状态提交与循环控制更新state到持久化存储Redis 或 DB判断是否done、failed、needs_human_review然后触发下一个 cycle 或退出提示很多团队把第 2 步Plan Generation和第 4 步Tool Execution混在一起让 LLM 直接输出{tool: send_email, to: xxx, body: ...}。这是大忌。Plan 必须可验证、可审计、可人工覆盖。我们线上系统要求任何 plan 在进入执行前必须通过PlanValidator类的 12 条规则校验例如“send_email” tool 的to字段必须匹配公司邮箱正则“search_db” 的limit参数不能超过 1000否则直接 reject不进 loop。2.2 “工具一直超时模型不停重试” —— 根本不是模型的问题是运行器的设计缺陷面试官问这个问题其实在考你有没有理解“运行器Runner” 的核心职责。它不是 LLM 的传声筒而是整个 Agent 的交通管制中心。当工具超时发生时模型根本没机会“重试”——因为重试决策权不在 LLM而在运行器的状态机里。我们来看一个真实 case某次对账机器人调用银联清算接口因对方限流返回 429但我们的运行器配置了timeout30s而银联实际响应时间波动在 25~45s。结果第 1 次发起请求 → 等待 30s → 触发 timeout → 记录error_type: tool_timeout→retry_count1第 2 次重新发起 → 等待 30s → 再次 timeout →retry_count2第 3 次同上 →retry_count3→ 运行器判定max_retries reached→ 执行 fallback切换到备用通道银联文件下载 API→ 成功关键点来了重试不是模型发起的是运行器根据state[retry_count]和预设策略自动触发的。模型在这个过程中只负责生成 plan不参与执行细节。如果让模型“看到”超时错误再决定重试等于把交通灯交给司机自己判断——路口必然瘫痪。所以一个健壮的运行器必须内置4 层重试策略策略层级触发条件动作实例工具级熔断同一 tool 连续 3 次失败含 timeout该 tool 熔断 60s期间所有请求返回 fallbacksearch_db熔断后自动查 Redis 缓存任务级退避retry_count max_retries指数退避第 1 次等 1s第 2 次等 2s第 3 次等 4s避免雪崩式重试冲击下游状态级降级retry_count max_retries切换到降级 plan预先配置的简化版 workflow主流程失败时启动“仅通知负责人”子流程人工介入通道retry_count max_retries且is_criticalTrue自动创建工单推送企业微信暂停该 task_id 所有后续步骤对账失败时必须人工确认才能继续注意max_retries绝不能硬编码在代码里。我们把它存在数据库的tool_config表中字段为default_max_retries和critical_max_retries。运维可在后台实时调整无需发版。曾有一次因某支付网关升级我们将pay_gateway的critical_max_retries从 3 改为 1当天故障率下降 73%。2.3 任务状态不是字符串是带版本号的结构化协议很多团队用status: running/failed这样的字符串管理状态这是埋雷。真实系统里任务状态必须是一个带版本、带上下文、带可追溯性的结构体。我们当前采用的TaskStateV2协议如下JSON Schema 片段{ task_id: tsk_abc123, version: 2.1, status: executing, phase: tool_execution, step_index: 2, current_tool: fetch_user_profile, retry_count: 1, timeout_at: 2024-06-15T14:22:30Z, last_error: { code: TOOL_TIMEOUT, message: HTTP request timeout after 30s, tool_call_id: tc_789, trace_id: tr_efgh }, execution_history: [ { step: 0, tool: validate_input, status: success, duration_ms: 12 } ], output: null }为什么这么复杂因为status字符串无法回答三个关键问题现在卡在哪一步→phasestep_indexcurrent_tool还能不能再试→retry_count对比max_retries后者存在独立配置表上次失败到底发生了什么→last_error包含完整上下文不是failed两个字我们曾遇到一个诡异 bug某个 Agent 在status: completed后又收到重复的 webhook 请求。排查发现是因为前端重发了请求而旧状态没做幂等校验。解决方案就是在TaskStateV2里加入request_id字段并在Task Ingestion阶段做 Redis SETNX 去重keytask:ingest:{request_id}ttl300s。这个设计让线上重复任务率从 0.8% 降到 0.002%。3. 工具调用不是发个 HTTP 请求是构建可信执行边界3.1 工具注册表比 LLM 的 system prompt 更重要的基础设施很多人以为工具调用就是写个tool装饰器然后扔给 LLM。错。工具注册表Tool Registry才是 Agent 的真实大脑。它必须解决四个问题谁能用权限控制sales_agent不能调finance_api怎么用参数校验send_email的to字段必须是白名单域名用得怎样监控指标成功率、P95 延迟、错误码分布坏了怎么办fallback 配置主 API 失败时自动切到 mock 数据或缓存我们用 PostgreSQL 建了一个tool_registry表核心字段如下字段类型说明tool_idVARCHAR(64)唯一标识如db_search_v2nameVARCHAR(128)显示名如 “客户数据库搜索”descriptionTEXTLLM 用来理解用途的描述会注入 system promptspec_jsonJSONBOpenAPI 3.0 格式的参数定义用于 runtime 校验endpointVARCHAR(255)实际调用地址支持变量替换如https://{{env}}.api.com/v1/searchtimeout_msINTEGER默认超时单位毫秒max_retriesINTEGER工具级最大重试次数覆盖全局设置fallback_strategyENUMnone/cache/mock/error_codeis_enabledBOOLEAN开关运维可实时启停owner_teamVARCHAR(64)责任团队故障时自动 实操心得spec_json字段必须用 JSON Schema v7 校验不能只靠 Python 的pydantic。我们曾因一个工具的spec_json缺少required字段导致 LLM 生成了缺失user_id的调用直接 500。后来加了 CI 检查所有 PR 提交的 tool spec必须通过jsonschema.validate()且required字段非空。3.2 超时控制不是 set_timeout(30)而是三层时间预算体系“工具超时”听起来简单实则最易出错。我们采用三层时间预算Time Budgeting体系确保每个环节都有明确时限LLM 生成时间预算从 prompt 发送到收到 completion上限 8sGPT-4 Turbo 实测 P99 为 3.2s留 4.8s 容错工具调用时间预算按工具类型分级设定数据库查询≤ 2s慢查询自动 kill外部 API≤ 15s银联类金融接口放宽至 30s文件处理≤ 60sPDF 解析单步总时间预算LLM_time Tool_time 序列化开销 ≤ 25s避免任务在队列中积压关键实现我们不用asyncio.wait_for()而是用Deadline Propagation模式。运行器在 dispatch step 时计算deadline now() tool_config.timeout_ms并将此 deadline 作为参数传给 tool executor。executor 内部所有操作DNS 查询、TCP 握手、SSL 握手、HTTP 读取都受同一 deadline 约束。Python 用asyncio.timeout()Go 用context.WithDeadlineJava 用CompletableFuture.orTimeout()。曾有一次某工具因 DNS 解析卡住上游 DNS 服务器故障requests.get()等了 90s 才超时。引入 deadline propagation 后该问题在 15s 内被强制中断错误日志清晰标记ERROR: DNS resolution timeout at step 3而不是模糊的ConnectionError。3.3 熔断与降级让 Agent 学会“战略性放弃”熔断不是简单的“失败三次就关闭”而是基于滑动窗口的动态决策。我们用 Redis Sorted Set 实现 60s 滑动窗口每次 tool 调用成功ZADD tool_failures:{tool_id} {timestamp} 0每次失败ZADD tool_failures:{tool_id} {timestamp} 1查询窗口内失败率ZRANGEBYSCORE tool_failures:{tool_id} {now-60} {now} WITHSCORES→ 计算sum(scores) / count熔断阈值配置在tool_registry表中例如db_search_v2:circuit_breaker_threshold 0.660% 失败率触发send_email:circuit_breaker_threshold 0.95邮件服务容错率更高降级策略分三级Level 1缓存查 Rediskeytool_cache:{tool_id}:{hash(args)}ttl300sLevel 2Mock返回预设的 JSON 结构字段值用 Faker 生成如{user_name: 张三, balance: 12345.67}Level 3Error Code返回标准错误对象{error: TOOL_UNAVAILABLE, fallback_used: mock}上层业务可据此降级 UI注意降级数据必须和真实数据结构一致。我们用 JSON Schema 生成 mock 数据确保前端不报Cannot read property email of undefined。曾因 mock 数据缺少profile.avatar_url字段导致 React 页面白屏教训深刻。4. 运行器不是胶水代码是 Agent 的操作系统内核4.1 运行器的四大核心能力调度、隔离、可观测、治理很多团队把运行器写成一个while True:循环这是重大误区。生产级运行器必须具备操作系统级别的能力能力说明我们的实现调度Scheduling决定哪个 task 优先执行、如何分配 CPU/内存资源基于 priority queue fair scheduler高优任务如支付抢占低优任务如报表生成资源隔离Isolation防止一个 task 故障影响其他 task每个 task 在独立 asyncio.Task 中运行超时自动 cancel内存使用量监控tracemalloc可观测Observability提供全链路 trace、metrics、loggingOpenTelemetry 标准接入每个 step 生成 spantag 包含tool_id,retry_count,is_fallback治理Governance强制执行安全策略、合规要求、成本控制注入cost_limit_usd字段每步执行前检查累计预估成本超限则 abort举个调度例子我们有一个report_generatorAgent平时 priority1但当财务总监在后台点击“紧急导出”时系统会将该 task 的 priority 动态提升到 10并触发资源重分配——暂停所有 priority5 的任务释放 CPU 给它。这个逻辑不是写在 Agent 里而是运行器的调度器模块。4.2 任务状态持久化的三种模式选错一种运维哭半年状态存哪这是架构分水岭。我们实践过三种模式结论很明确模式适用场景优点缺点我们的选择内存状态In-Memory本地 demo、单机测试极简零延迟进程崩溃即丢失无法水平扩展❌ 永远不用RedisKey-Value中等规模、高吞吐、容忍短暂不一致亚毫秒延迟天然支持分布式锁复杂状态序列化麻烦无事务保证✅ 主力方案用 RedisJSON 存TaskStateV2PostgreSQLACID金融级、强一致性、需审计溯源事务可靠SQL 查询灵活天然备份延迟高~5ms连接池压力大✅ 关键任务兜底如对账任务写双写Redis PG关键细节Redis 不是简单SET task:123 {...}。我们用Redis Streams存事件日志task_created,step_started,tool_failed用RedisJSON存当前状态快照。这样既保证高性能又能通过XREAD回溯任意时刻状态。曾有一次因 Redis 内存不足触发淘汰task:123key 被删但 Streams 里的事件还在我们用XREAD重建了状态挽回了 3 小时的数据。4.3 错误处理的黄金法则永远不要让 LLM 看到 raw error这是血泪教训。早期我们把requests.exceptions.Timeout直接塞给 LLM结果模型生成了离谱回复“检测到网络超时请重启路由器”。后来我们建立Error Normalization Layer捕获原始异常如requests.Timeout,psycopg2.OperationalError映射为标准化错误码TOOL_TIMEOUT,DB_CONNECTION_REFUSED注入领域语义The payment gateway is currently unreachable. Please try again in 5 minutes.附加 action guidance{suggested_action: retry_after_delay, delay_seconds: 30}LLM 只能看到第 3 步的自然语言描述 第 4 步的结构化指令绝不会看到 stack trace。这样模型才能专注做决策而不是 debug 网络。我们维护一个error_mapping.yamlrequests.Timeout: code: TOOL_TIMEOUT message: The external service did not respond in time. action: retry_with_backoff psycopg2.OperationalError: code: DB_UNAVAILABLE message: Database connection failed. This may be temporary. action: use_cache_fallback实操心得action字段必须是运行器能识别的枚举值不能是自由文本。我们定义了 7 种标准 actionretry_immediately,retry_with_backoff,use_cache_fallback,switch_to_alternative_tool,escalate_to_human,abort_and_notify,skip_and_continue。LLM 的输出里只要包含action: retry_with_backoff运行器就知道该怎么做不需要 parse 自然语言。5. 常见问题与排查技巧实录那些凌晨三点教会我的事5.1 典型问题速查表现象可能原因排查命令/方法解决方案agent execution terminated due to error.频繁出现max_retries设置过低或工具本身不稳定查task_state表统计last_error.code分布调高max_retries或为该 tool 配置fallback_strategy: cacheAgent 卡在executing状态不动某 step 的timeout_ms设为 0无限等待redis-cli KEYS task:*→JSON.GET task:xxx $.phase在 tool registry 表中修正timeout_ms加 CI 检查禁止 0 值任务堆积Redis 内存暴涨execution_history未清理或TaskStateV2过大redis-cli MEMORY USAGE task:xxx→JSON.DEBUG MEMORY task:xxx设置execution_history最大长度为 20旧记录自动 trimLLM 反复生成相同 tool callPlan 生成阶段未禁用已失败的 tool查state.plan.steps看是否重复出现search_db在 Plan Generation prompt 中加入“Avoid tools that failed in previous steps: [db_search_v2]”多个 Agent 同时修改同一数据库记录产生脏写缺少乐观锁或分布式锁查 DB binlog看 update 语句 timestamp 是否重叠在tool_executor层加SELECT ... FOR UPDATE或用 Redis lock5.2 独家避坑技巧来自 327 份错误日志的总结技巧 1给每个 tool call 打上唯一 trace_id不要依赖 LLM 生成的tool_call_id它可能重复或缺失。我们在运行器 dispatch 时用uuid.uuid4().hex[:8]生成tool_call_id并注入到所有日志、metric tag、span context 中。这样当agent execution terminated due to error.出现时运维只需搜tool_call_id: abcd1234就能串联起 LLM prompt、HTTP request、DB query 全链路。技巧 2用 “状态快照 diff” 定位静默失败有些失败不抛异常只是返回空数据。我们在TaskStateV2里加了state_diff字段每次 commit 前计算新旧 state 的 JSON diff用jsonpatch库。如果output从null变成{}空对象而status还是executing就触发告警——这往往是工具返回了 200 但 body 为空。技巧 3为重试加 “指纹” 防止雪崩单纯指数退避不够。我们给每次重试加 fingerprintfingerprint hash(f{tool_id}_{args_json}_{retry_count})。如果 5 分钟内同一 fingerprint 出现 3 次运行器自动熔断该组合避免因参数错误导致的无限重试。技巧 4用 “影子模式” 灰度新 tool上线新工具前先以 shadow mode 运行正常流程走老 tool同时异步调用新 tool对比结果。只有当新 tool 连续 100 次结果一致才切流量。我们曾用此法发现新天气 API 在city北京时返回上海数据的 bug。技巧 5强制 “失败必告警成功必记录”所有state.status failed的任务必须触发企业微信告警含task_id,last_error.message,trace_id。所有state.status completed的任务必须写入task_audit_log表含output_size_bytes,total_duration_ms,llm_token_usage。没有例外。这是 SLO 的底线。5.3 面试官真正想听的答案长这样回到开头那个差点吵起来的问题“一个 Agent 收到任务以后是怎么一步步执行的如果工具一直超时模型不停重试怎么办”正确答案不是背诵流程而是展示你的系统思维“首先这不是模型在重试是运行器在执行预设的熔断-退避-降级策略。比如当search_db工具连续 3 次超时运行器会触发熔断后续请求直接返回 Redis 缓存同时retry_count达到上限后运行器不会让模型再试而是加载降级 plan —— 比如改用 Elasticsearch 的近似搜索。整个过程模型只负责生成初始 plan不参与执行决策。真正的智能体现在运行器的状态机设计、工具注册表的治理能力、以及错误归一化层的语义提炼上。”说完可以补一句“如果您感兴趣我可以现场用 5 分钟基于langgraph搭一个带熔断和降级的最小可行 Agent演示agent execution terminated due to error.如何被拦截并优雅降级。”这才是面试官想看到的——不是知道名词而是亲手造过轮子并且知道轮子在哪会爆胎、怎么换、换哪种型号。
返回列表