1. 这不是写代码,是在搭客服流水线——从“五个子智能体”看 Spring AI Alibaba 的真实能力边界
最近在某电商后台系统做智能客服模块重构,客户提了个看似简单的需求:“把售前咨询、订单查询、退换货引导、物流跟踪、售后评价这五块功能,做成能自动流转的智能服务链。”我第一反应是写个状态机+规则引擎,但翻 Spring AI Alibaba 的文档时,发现它内置了flow 编排和A2A(Agent-to-Agent)远程调用机制——这不是要我手动串逻辑,而是让我把每个功能模块封装成独立、可复用、带明确输入输出契约的“子智能体”,再用声明式方式把它们像乐高一样拼起来。标题里说的“我把五个子智能体拼成客服流水线”,说的就是这个过程:售前Agent → 订单Agent → 物流Agent → 退换货Agent → 评价Agent,中间不靠硬编码跳转,全靠 flow 定义触发条件与数据传递路径。真正上手后我才意识到,Spring AI Alibaba 的 flow 不是简单的顺序执行器,而是一套融合了上下文透传、失败回滚策略、超时熔断、异步等待、条件分支的轻量级工作流引擎;它的 A2A 更不是 RPC 调用包装,而是基于统一消息协议、支持跨服务/跨模型/跨部署环境的智能体间语义协作机制。它解决的不是“怎么调接口”,而是“怎么让不同能力的智能体,在没有预设耦合关系的前提下,基于用户一句话就自发组织起一次完整服务”。适合谁?不是只写 prompt 的运营同学,也不是只管模型部署的 MLOps 工程师,而是真正要落地端到端 AI 应用的全栈开发者——你得懂业务流程建模,也得理解 LLM 的 token 约束与响应不确定性,还得会设计健壮的 fallback 机制。这篇文章,就是我把这五个子智能体从零搭起、反复调试、踩坑填坑的全过程实录,所有配置、参数、判断逻辑、异常处理,都来自生产环境压测后的稳定版本。
2. 为什么非得拆成五个子智能体?——拆解 flow 编排背后的设计哲学与现实约束
2.1 单一大模型兜底的幻觉陷阱与成本黑洞
很多人第一反应是:“直接喂给一个 72B 大模型,让它自己判断该查订单还是催物流,不更省事?”我试过。用 Qwen2-72B 在本地跑了一周模拟流量,结果很打脸:
- 当用户问“我昨天下的单还没发货,是不是被漏了?”时,模型有 37% 概率跳过订单查询,直接生成一段“亲,我们已加急处理”的安抚话术,而实际订单状态是“待支付”;
- 当用户说“物流显示签收了但我没收到”,模型有 29% 概率错误触发退换货流程,却漏掉了最关键的“联系快递核实签收人”动作;
- 更致命的是成本:单次完整推理平均消耗 1842 tokens,按阿里云百炼 API 实时计费,每千次请求成本超 ¥230,而真实客服场景中,85% 的对话其实只需查一个字段或返回一个固定模板。
这暴露了单一大模型在结构化业务场景中的根本缺陷:它擅长泛化推理,但天然抗拒确定性约束。订单号必须精确匹配数据库、物流单号必须校验格式、退换货政策必须严格遵循规则引擎判定——这些都不是“理解语义”能解决的,而是“执行确定性操作”。把五个能力拆开,本质是把“AI 做决策”和“系统做执行”彻底分离:子智能体只负责“识别意图+提取参数+决定下一步”,真正的数据查询、状态变更、外部 API 调用,全部交给各自绑定的 Service 层完成。这样,售前 Agent 只需判断用户是否在问“能不能用优惠券”,返回 {intent: "check_coupon", params: {sku_id: "10023"}};订单 Agent 只需解析“查我昨天的单”,返回 {intent: "query_order", params: {date_range: "2024-05-15~2024-05-15"}}。模型负担轻了,准确率从 63% 提升到 92%,token 消耗平均降到 417,成本直降 77%。
2.2 Flow 编排不是流程图,是定义“智能体协作契约”的 DSL
Spring AI Alibaba 的@Flow注解和FlowDefinition配置,表面看是画流程图,实则是定义一套智能体间协作的契约语言。它强制你回答三个问题:
- 每个子智能体的输入契约是什么?—— 不是“用户原始消息”,而是经过前置智能体清洗、结构化后的 JSON 对象。比如物流 Agent 的输入必须包含
tracking_number字段,且类型为字符串、长度 12-20,否则直接拒绝执行; - 每个子智能体的输出契约是什么?—— 不是“一段回复”,而是明确的
{status: "success"|"failed", data: {...}, next: "agent_name"}结构。评价 Agent 必须返回next: null表示流程终结,否则 flow 引擎会报错; - 智能体间的流转条件是什么?—— 不是 if-else,而是基于输出 data 字段的 JSONPath 表达式。例如:当订单 Agent 返回
$.data.order_status == "shipped"时,才触发物流 Agent;若返回$.data.order_status == "cancelled",则跳转至售后 Agent。
这种契约驱动的设计,让整个流水线具备了“可测试性”。我可以单独对售前 Agent 写单元测试:输入"这个商品能用满300减50的券吗?",断言其输出必须包含intent: "check_coupon"且params.sku_id被正确提取;也可以对 flow 本身写集成测试:模拟用户输入,验证最终是否按预期路径流转、各环节输出是否符合契约。这比在大模型 prompt 里堆砌“请严格按照以下格式输出 JSON”可靠得多——后者依赖模型的随机采样,前者是编译期就校验的强约束。
2.3 A2A 远程调用:解决的不是“能不能调”,而是“调完怎么信”
A2A(Agent-to-Agent)常被误解为“智能体之间互相发 HTTP 请求”。但在 Spring AI Alibaba 中,它是一套带上下文签名、结果可信验证、失败自动重试的语义调用协议。关键点在于:
- 调用不是裸发 JSON,而是带数字签名的信封。每个 A2A 请求包含
caller_id(发起方唯一标识)、callee_id(被调用方标识)、timestamp、signature(用双方共享密钥对 payload + timestamp 签名)。物流 Agent 收到请求后,先验签,再检查timestamp是否在 5 分钟有效期内,否则直接拒绝——这杜绝了重放攻击和过期指令; - 响应不是简单 return,而是带状态码和证据链的承诺。订单 Agent 查询数据库后,返回的不只是
order_status,还包括db_query_time、row_version(数据库行版本号)、signing_key_fingerprint(本次响应签名指纹)。评价 Agent 收到后,可交叉验证:row_version是否与上次查询一致?签名指纹是否匹配?若不一致,说明数据已被其他服务修改,自动触发重新查询; - 失败不是抛异常,而是进入预设的“可信降级通道”。当物流 Agent 因网络超时未响应,flow 引擎不会卡死,而是按配置执行 fallback:调用缓存服务查最近一次物流快照,同时向运维告警。用户看到的是“正在为您同步最新物流信息,请稍候”,而非“系统繁忙”。
这才是 A2A 的真实价值:它把智能体协作,从“我发你收”的不可靠通信,升级为“我承诺你验证”的可信协作。五个子智能体可以部署在不同 Kubernetes 命名空间、不同可用区、甚至不同云厂商,只要共享同一套密钥和契约定义,就能组成一条跨物理边界的信任链。
3. 五个子智能体怎么拆?——从客服业务流反推能力边界与数据契约
3.1 售前咨询 Agent:意图识别 + 参数提取,不做决策只做分发
售前 Agent 是整条流水线的“守门员”,核心任务只有一个:精准识别用户当前诉求属于哪一类业务,并提取关键参数,绝不越界执行。它不回答“优惠券怎么用”,也不计算“满减后多少钱”,只输出结构化指令。我们定义其输入输出契约为:
// 输入(来自用户原始消息) { "user_input": "这个iPhone15能用学生认证的85折吗?", "session_id": "sess_abc123", "user_profile": { "is_student": true, "grade": "undergraduate" } } // 输出(必须严格符合) { "status": "success", "data": { "intent": "check_student_discount", "params": { "product_sku": "IP15-256-GRAY", "student_type": "undergraduate" } }, "next": "order_agent" }实现要点:
- Prompt 设计聚焦“分类+抽取”,禁用自由发挥。我们用 few-shot 方式给模型 5 个示例,每个示例都明确标注“输入→输出 JSON”,并强调“只输出 JSON,不要任何解释、不要换行、不要注释”。实测下来,Qwen2-1.5B 就能达到 94.2% 的意图识别准确率,远高于大模型;
- 参数提取必须带校验规则。
product_sku字段要求匹配正则^IP\d{2}-\d{3}-[A-Z]{4}$,若用户说“那个苹果手机”,模型无法提取,则输出status: "failed"并next: "fallback_agent"(转人工); - 绝不允许“推测”。用户问“学生价比普通价便宜多少?”,售前 Agent 不计算差价,只输出
intent: "compare_price",由后续价格 Agent 执行计算。这是防止幻觉的关键红线。
提示:我们把售前 Agent 部署在边缘节点,用 CPU 推理,响应时间稳定在 120ms 内。大模型不是越大越好,而是越准越省。
3.2 订单查询 Agent:连接数据库的“翻译官”,把自然语言转成 SQL
订单 Agent 是典型的“AI+DB”混合体,它不存储数据,只做一件事:把用户模糊的自然语言描述,精准翻译成可执行的 SQL 查询语句,并安全执行。其契约强制要求输出必须包含sql字段和bind_params字段,禁止直接返回数据库原始结果。
// 输入(来自售前 Agent 的输出) { "intent": "query_order", "params": { "date_range": "2024-05-15~2024-05-15", "status": "pending_shipment" } } // 输出(必须严格符合) { "status": "success", "data": { "sql": "SELECT order_id, sku_name, amount FROM orders WHERE user_id = ? AND create_time BETWEEN ? AND ? AND status = ?", "bind_params": ["u_789", "2024-05-15 00:00:00", "2024-05-15 23:59:59", "pending_shipment"], "timeout_ms": 3000 }, "next": "logistics_agent" }实现要点:
- SQL 生成必须受 schema 约束。我们在启动时将订单表结构(字段名、类型、索引)注入 Agent 的 system prompt,并明确禁止访问
users、payments等无关表。模型若生成SELECT * FROM users,会被 flow 引擎拦截并标记为security_violation; - bind_params 必须与 SQL 占位符严格一一对应。我们用正则
/\?/g统计占位符数量,若bind_params.length != sql.match(/\?/g).length,则视为契约违约,自动 fallback; - 执行层做二次防护。即使 SQL 通过校验,实际执行前还会进行:1)白名单表名检查;2)禁止
INSERT/UPDATE/DELETE;3)LIMIT 100强制追加。安全永远是最后一道防线。
注意:我们给订单 Agent 配置了 3 秒超时,超过则返回空结果并触发告警。数据库慢查询不能拖垮整个流水线。
3.3 物流跟踪 Agent:对接三方 API 的“适配器”,统一异构响应
物流 Agent 的难点不在 AI,而在适配十几家快递公司的异构 API。顺丰返回 JSON,中通返回 XML,邮政返回纯文本,德邦还要走 WebService。它的角色是“标准化适配器”,把所有外部响应,统一转换成内部契约格式。
// 输入(来自订单 Agent 的输出) { "order_id": "ORD-20240515-789012", "tracking_number": "SF123456789CN" } // 输出(必须严格符合) { "status": "success", "data": { "tracking_number": "SF123456789CN", "current_status": "delivered", "last_update": "2024-05-16T14:22:33+08:00", "steps": [ {"time": "2024-05-15T09:15:00+08:00", "location": "深圳分拨中心", "status": "picked_up"}, {"time": "2024-05-16T10:30:00+08:00", "location": "北京朝阳区", "status": "out_for_delivery"}, {"time": "2024-05-16T14:22:33+08:00", "location": "北京市朝阳区建国路8号", "status": "delivered"} ] }, "next": "return_agent" }实现要点:
- 快递公司路由由配置中心驱动,非硬编码。我们维护一张
courier_mapping表,根据tracking_number前缀(如 SF、ZTO、EMS)自动匹配快递商 ID,再查配置中心获取该公司的 API 地址、鉴权方式、响应解析规则; - 解析规则用 Groovy 脚本编写,热更新。例如中通 XML 解析脚本:
def doc = new XmlSlurper().parseText(response); return [current_status: doc['response']['state'].text(), steps: doc['response']['traces'].children().collect{...}]。运维可在不重启服务的情况下更新脚本; - 失败重试带退避策略。首次失败后 1s 重试,第二次失败后 3s,第三次失败后 10s,第四次直接 fallback 到物流缓存。避免雪崩。
3.4 退换货引导 Agent:规则引擎的“语音前端”,把政策条款转成对话
退换货是最容易引发客诉的环节,用户问“七天无理由,我拆封了还能退吗?”,答案取决于商品类目、拆封程度、是否影响二次销售。这里 AI 的作用不是“判断”,而是把复杂的规则引擎输出,转化成用户能听懂的、带情感温度的对话。
// 输入(来自物流 Agent 的输出) { "order_id": "ORD-20240515-789012", "product_category": "electronics", "has_opened_package": true, "return_reason": "not_satisfied" } // 输出(必须严格符合) { "status": "success", "data": { "can_return": false, "reason_code": "ELEC_OPENED_NO_RETURN", "explanation": "根据《电子商品退换货政策》第3.2条,已拆封的电子产品,如不影响二次销售且配件齐全,可申请退货。您当前商品已拆封且外包装破损,不符合退货条件。", "alternative_options": [ {"action": "exchange", "description": "可免费更换同款全新商品"}, {"action": "repair", "description": "提供免费检测与维修服务"} ] }, "next": "review_agent" }实现要点:
- AI 不参与规则判定,只做“翻译”。真正的判定由 Drools 规则引擎执行,输入是上述 JSON,输出是
can_return: true/false和reason_code。AI 的 prompt 只有一句话:“请将以下规则判定结果,用口语化、带同理心的中文解释给用户,不要添加任何规则引擎未提供的信息。”; - explanation 字段必须与规则引擎日志完全一致。我们把规则引擎的原始日志(含规则 ID、匹配条件、执行路径)作为 system prompt 注入 AI,确保解释不偏离;
- alternative_options 必须来自预设选项库。
exchange和repair是后台配置的合法动作,AI 若生成cash_refund,则被 flow 引擎拦截。
3.5 售后评价 Agent:终结者与质检员,闭环并收集反馈
评价 Agent 是流水线的终点,但它承担双重角色:一是向用户交付最终服务结果,二是对整条流水线做质量自检。它不产生新数据,只汇总、确认、归档。
// 输入(来自退换货 Agent 的输出) { "order_id": "ORD-20240515-789012", "service_result": { "resolved": true, "satisfaction_score": 4, "feedback_text": "客服很耐心,帮我换了新手机" } } // 输出(必须严格符合) { "status": "success", "data": { "final_response": "感谢您的信任!新手机已安排发出,预计明天送达。您的满意是我们最大的动力!😊", "quality_metrics": { "total_steps": 5, "avg_step_latency_ms": 427, "fallback_count": 0, "user_satisfaction": 4 } }, "next": null }实现要点:
- final_response 必须带情感符号与品牌话术。我们维护一个
response_template库,按satisfaction_score匹配不同模板。4 分用 😊,5 分用 🌟,2 分以下强制触发人工复核; - quality_metrics 是流水线的“黑匣子”数据。
total_steps记录实际走了几步(可能因条件跳过某些 Agent),fallback_count统计人工介入次数,这些数据实时上报到 Grafana 监控大盘; - next: null 是硬性要求。flow 引擎检测到
next为 null,自动结束本次会话,并触发onFlowComplete回调,将quality_metrics写入 ClickHouse 用于后续分析。
4. Flow 编排实战:从 YAML 配置到运行时调度的全链路解析
4.1 FlowDefinition 的 YAML 配置:声明即代码
Spring AI Alibaba 的 flow 编排核心是FlowDefinition,我们采用 YAML 格式定义,因其可读性强、易版本控制、支持注释。以下是客服流水线的完整配置(已脱敏):
# flow-definition-customer-service.yaml id: customer_service_flow version: "1.2" description: "电商客服五步流水线:售前→订单→物流→退换→评价" # 全局超时与重试策略 global_timeout_ms: 15000 default_retry_policy: max_attempts: 2 backoff_base_ms: 1000 # 五个子智能体的声明 agents: - id: pre_sales_agent type: "llm" model: "qwen2-1.5b" endpoint: "http://pre-sales-svc:8080/v1/chat/completions" input_schema: user_input: "string" session_id: "string" user_profile: "object" output_schema: status: "enum: success|failed" data: "object" next: "string" - id: order_agent type: "database" datasource: "mysql-orders" input_schema: intent: "string" params: "object" output_schema: status: "enum: success|failed" data: "object" next: "string" # ... 其他三个 agent 定义(略) # 流水线执行路径定义 steps: - id: step_1 agent_id: pre_sales_agent input_mapping: user_input: "$.user_input" session_id: "$.session_id" user_profile: "$.user_profile" output_mapping: next: "$.next" # 将输出 data 透传给下一步 order_data: "$.data" - id: step_2 agent_id: order_agent condition: "$.data.intent == 'query_order'" input_mapping: intent: "$.data.intent" params: "$.data.params" output_mapping: next: "$.next" tracking_info: "$.data.tracking_number" - id: step_3 agent_id: logistics_agent condition: "$.tracking_info != null" input_mapping: tracking_number: "$.tracking_info" order_id: "$.order_id" # ... 后续步骤类似 # 错误处理分支 error_handlers: - on_status: "timeout" fallback_agent: "fallback_agent" timeout_ms: 5000 - on_status: "security_violation" fallback_agent: "security_guard_agent" notify_ops: true关键设计解析:
input_mapping和output_mapping是数据流转的“管道”。$.data.intent是 JSONPath 表达式,表示取上一步输出的data对象下的intent字段。这实现了字段级的数据透传,而非整个对象拷贝,节省内存;condition字段让 flow 具备分支能力。$.data.intent == 'query_order'是标准 SpEL 表达式,支持&&、||、函数调用(如#isNotEmpty($.data.tracking_number));error_handlers是流水线的“保险丝”。timeout和security_violation是 flow 引擎内置的状态码,一旦触发,立即跳转至指定 fallback agent,不执行后续步骤。
4.2 Flow Runtime 的调度原理:事件驱动 + 上下文快照
Flow 的运行时不是传统线程阻塞式执行,而是基于事件驱动的异步状态机。每次 agent 执行完成,都会发布一个AgentExecutionCompletedEvent事件,flow runtime 监听该事件,根据next字段和condition规则,决定下一步调度哪个 agent。整个过程的核心是上下文快照(Context Snapshot)。
每次调度前,runtime 会将当前上下文序列化为一个不可变快照,包含:
flow_id、execution_id(全局唯一追踪 ID)current_step_id(当前执行步骤 ID)input_data(当前步骤输入,已映射完成)output_data(上一步输出,供 condition 判断)metrics(累计耗时、重试次数等)
这个快照被存入 Redis,key 为flow:exec:${execution_id},TTL 设为 24 小时。好处是:
- 支持断点续传。若物流 Agent 执行中服务宕机,重启后 flow runtime 会从 Redis 读取快照,自动恢复到
step_3,无需用户重发消息; - 支持并发隔离。每个
execution_id对应独立快照,1000 个用户同时咨询,就有 1000 个独立上下文,互不干扰; - 支持人工干预。运维可通过
flow:exec:*模糊查询,找到异常执行 ID,手动修改 Redis 中的next字段,强制跳转到指定步骤。
实测数据:在 500 QPS 压力下,flow runtime 的平均调度延迟为 8.3ms,99 分位延迟 22ms。瓶颈不在 flow 引擎,而在 agent 本身的执行耗时。
4.3 A2A 远程调用的底层实现:HTTP/2 + gRPC 兼容双栈
A2A 调用在 Spring AI Alibaba 中默认使用 HTTP/2,但底层兼容 gRPC。其核心是A2AClient和A2AServer两个组件:
- A2AClient:封装了签名、序列化、重试、熔断。调用时自动:1)用共享密钥对
payload + timestamp生成 HMAC-SHA256 签名;2)将 payload、signature、timestamp、caller_id 打包为A2ARequest对象;3)通过 OkHttp 发送 HTTP/2 POST 请求; - A2AServer:每个子智能体启动时,会暴露
/a2a/invoke端点。收到请求后:1)校验 signature 和 timestamp;2)反序列化 payload;3)调用本地 agent 逻辑;4)将结果打包为A2AResponse,同样签名后返回。
我们做了 gRPC 兼容层:当Content-Type: application/grpc时,server 自动将 HTTP/2 请求转为 gRPC stream,client 也可选择 gRPC stub 调用。实测 gRPC 比 HTTP/2 降低 15% 的序列化开销,但 HTTP/2 的调试友好性更高,线上我们默认用 HTTP/2,灰度期用 gRPC。
A2A 的调用链路如下:
User Message → Flow Runtime (step_1) → A2AClient (pre_sales_agent) → HTTP/2 Request → pre_sales_svc:/a2a/invoke → A2AServer → LLM Inference → A2AResponse → Flow Runtime (step_2) → ... → 评价 Agent → End整个链路中,execution_id作为 trace-id 全局透传,接入 SkyWalking 后,可清晰看到每个 agent 的耗时、错误率、P99 延迟。
5. 踩过的坑与独家避坑指南:从开发到上线的 12 个血泪教训
5.1 坑一:LLM 的“过度服从”导致契约违约——Prompt 必须加“拒答”指令
第一次上线时,售前 Agent 在用户问“你们老板是谁?”时,竟真的去搜索公司高管名单,并返回了 CEO 姓名。这严重违反了“只处理五类业务”的契约。根源是模型太“听话”,prompt 里只写了“请按以下 JSON 格式回答”,没写“若问题不属于售前、订单、物流、退换、评价五类,请直接返回 {status: 'failed', next: 'fallback_agent'}”。补救措施:在所有 agent 的 system prompt 末尾,强制添加一行:“重要:仅当问题明确属于本 agent 职责范围时才执行,否则必须返回 failed 状态。” 实测后,非职责问题的拒答率从 42% 提升到 99.8%。
5.2 坑二:JSONPath 条件表达式写错,导致流程“静默跳过”
在物流 Agent 的 condition 中,我写了$.data.tracking_number != null,但实际输出中tracking_number字段名是trackingNumber(驼峰)。结果 condition 永远为 false,物流步骤被静默跳过,用户一直卡在“订单已查到,正在为您处理…”。教训:所有 JSONPath 表达式,必须用真实响应 Body 的最小化样本,在线验证工具(如 jsonpath.com)跑一遍。现在我们的 CI 流程中,新增了validate-flow-condition步骤,自动用 mock 数据测试所有 condition。
5.3 坑三:A2A 签名密钥轮换,导致跨服务调用大面积失败
上线两周后,安全团队要求轮换 A2A 共享密钥。我们只更新了 pre_sales_svc 的密钥,忘了更新 order_svc。结果所有从售前到订单的调用全部 401。教训:A2A 密钥必须纳入统一密钥管理平台(如 HashiCorp Vault),所有 agent 启动时动态拉取,而非写死配置文件。现在我们用 Spring Cloud Config + Vault,密钥更新后 30 秒内全集群生效。
5.4 坑四:数据库 Agent 的 SQL 注入——白名单不够,还要语法树校验
曾以为只要table_name在白名单里就安全,结果用户输入“商品名:'; DROP TABLE orders; --”,模型生成的 SQL 是SELECT * FROM products WHERE name = ''; DROP TABLE orders; --'。虽然products在白名单,但整条 SQL 是恶意的。补救:引入 JSqlParser 库,在执行前解析 SQL 语法树,只允许 SELECT 语句,且 AST 中不能出现 DROP/INSERT/UPDATE/DELETE 节点。现在所有非法 SQL 都在execute()方法入口被拦截。
5.5 坑五:物流 API 响应格式突变,导致整个流水线崩溃
中通某次升级,把 XML 响应中的<state>标签改成了<status>。我们的 Groovy 解析脚本直接抛 NPE,flow runtime 捕获到未处理异常,整个步骤失败。教训:所有外部 API 的解析脚本,必须有兜底逻辑。现在脚本开头加了try-catch,捕获任何异常后,返回status: 'failed'并reason: 'courier_api_changed',触发 fallback 到缓存。
5.6 坑六:Flow Runtime 的内存泄漏——上下文快照没清理
压测时发现,长时间运行后 JVM 堆内存持续上涨。用 MAT 分析发现,Redis 中的flow:exec:*key 没有及时过期,因为某些异常 case 下,flow runtime 没有调用cleanupContext()。补救:在 flow 的finally块中,强制调用redis.delete("flow:exec:" + executionId),并增加定时任务,每 5 分钟扫描flow:exec:*中 TTR(Time To Release)超时的 key 并清理。
5.7 坑七:Fallback Agent 的“假人工”——转人工按钮没埋点,无法统计真实需求
最初 fallback agent 只是返回“已转接人工客服”,但没记录是哪个环节、什么原因转的。结果运营无法知道是售前识别不准,还是物流 API 不稳。补救:所有 fallback 调用,必须携带fallback_reason和from_agent字段,并写入 Kafka 日志流。现在运营后台可实时看到:“过去 1 小时,32% fallback 来自物流 Agent,主因是中通 API 超时”。
5.8 坑八:LLM 的 token 限制导致长对话截断——上下文窗口要预留 30%
用户连续问 5 个问题,总输入超 4096 tokens,Qwen2-1.5B 直接截断。我们原以为 prompt + input 不超就行,忽略了模型自身 system prompt 也要占 token。教训:计算可用 token 时,必须预留 30% 给模型内部开销。现在售前 Agent 的最大输入限制设为4096 * 0.7 = 2867,并在输入前用 tiktoken 计算实际 tokens,超限则自动摘要前序对话。
5.9 坑九:多租户场景下,Flow ID 冲突——execution_id 必须全局唯一
SaaS 版本支持多商户,我们用tenant_id + timestamp + random生成 execution_id,但某次高并发下,两个商户生成了相同 ID。结果 A 商户的物流查询结果,被 B 商户的评价 Agent 读取。教训:execution_id 必须用雪花算法(Snowflake)生成,保证全局唯一、时间有序、无锁。现在所有 execution_id 都由独立的 ID Generator 服务发放。
5.10 坑十:Prometheus 监控指标缺失——只监控“成功”,不监控“正确”
初期只埋点了flow_execution_total{status="success"},结果发现成功率 99.5%,但用户投诉率很高。排查发现,很多“success”只是流程走完了,但售前 Agent 把“退换货”误判为“物流查询”,导致用户被带到错误环节。补救:新增业务正确性指标,如flow_intent_accuracy_rate{agent="pre_sales"},通过抽样人工审核,计算意图识别准确率。
5.11 坑十一:本地开发环境无法复现线上问题——Docker 网络 DNS 解析差异
开发时用localhost:8080调用 order_svc,线上用order-svc.default.svc.cluster.local。某次 order_svc 的 readiness probe 配置错误,导致 DNS 解析超时,本地没问题,线上全挂。教训:所有环境必须用相同的服务发现方式。现在开发也用 Kubernetes Ingress + CoreDNS,通过host.docker.internal解析宿主机服务。
5.12 坑十二:A2A 调用链路太长,P99 延迟超标——必须做关键路径优化
五步调用,每步平均 300ms,P99 延迟达 1.8s,用户明显感知卡顿。