1. 什么是 AI Agent?它不是“更聪明的聊天机器人”,而是能闭环做事的数字员工
你肯定见过这样的场景:在某个技术群里,有人发截图说“我用 LangChain 写了个自动订会议室的 Agent,今天上午跑了 37 次,成功 36 次”;另一条消息是“刚上线的客服 Agent,在测试环境里卡在 tool calling 步骤不动了,日志里只有一行tool not found,但明明注册了三个 function”;还有人吐槽:“扣子上拖拽出来的 Agent 看着很炫,一到真实业务里就崩——用户问‘帮我查下上个月第三笔退款进度’,它直接返回‘抱歉,我无法理解您的请求’”。这些不是故障,而是典型症状。它们共同指向一个事实:AI Agent 的本质,从来不是“调用大模型回答问题”,而是构建一个具备感知、决策、执行、反馈能力的闭环系统。它要像人一样,先听懂(理解 query),再想清楚(规划路径),然后动手做(调用工具),做完还要回头看(验证结果),错了还得重来(循环修正)。这和传统 Web API 或单次 LLM 推理有根本区别——后者是一锤子买卖,Agent 是个持续运转的“小工厂”。
我做过 12 个落地项目,从金融风控辅助 Agent 到工业设备巡检 Agent,最深的体会是:90% 的失败,不是因为模型不够强,而是因为工程设计没想清楚“谁在什么时候、基于什么信息、做出什么决定”。比如,一个电商售后 Agent,用户说“我要退那件蓝色连衣裙”,它得先识别这是退货请求(意图识别),再确认订单号(需要记忆或检索),再查库存状态(调用 ERP 接口),再判断是否支持无理由(业务规则),最后生成退款单并通知物流(多步执行)。这中间任何一个环节决策错位——比如把“退货”误判成“换货”,或者在没查到订单号前就去调用退款接口——整个流程就断了。所以,“解构 AI Agent”这件事,不能只谈 prompt 工程或框架选型,必须回到最底层:它内部到底有几个关键决策点?每个点上,系统靠什么信息做判断?判断的依据又是什么?这就是标题里说的“七要素”与“七个决策点”的关系:七要素(如 Memory、Tool、Orchestration)是静态组件,而七个决策点是动态过程——是这些组件在运行时被真正“激活”的时刻。就像一辆车有发动机、变速箱、方向盘(要素),但真正决定它往左还是往右、加速还是刹车的,是司机在路口、坡道、红绿灯前的七次关键判断(决策点)。本文不讲抽象理论,只讲我在生产环境里踩过坑、改过三次架构、最终跑通 200+ QPS 的真实决策逻辑。如果你正卡在 Agent 总是“答非所问”“调用失败”“无限循环”,或者纠结该用 LangGraph 还是 LlamaIndex,那接下来的内容,就是你缺的那张施工图。
2. 七要素不是功能列表,而是 Agent 的“器官系统”——每个都对应一个不可跳过的决策点
很多人把 AI Agent 的七要素(通常指 Planning、Memory、Tool Use、Execution、Observation、Reflection、Orchestration)当成模块清单,以为装上 LangChain 就自动有了。错。这七个词,本质上描述的是 Agent 在一次完整任务中,必须经历的七个生理级决策环节。它们不是并列的“功能开关”,而是环环相扣的“神经反射弧”。我拿一个最简单的例子说明:用户输入“把上周五会议的待办事项同步到飞书多维表格”。这个请求看似简单,但 Agent 的内部决策链是这样的:
决策点 1:意图识别与任务拆解
它得先判断:这是单任务(同步)还是复合任务(先找会议记录,再提取待办,再写入表格)?用户没说“找会议记录”,但 Agent 必须自己推断出这一步。这里用的不是关键词匹配,而是基于 LLM 的 zero-shot 分类 + 结构化输出约束(比如强制输出 JSON 格式{"task_type": "sync", "source": "meeting_notes", "target": "feishu_table"})。我实测过,如果不用结构化输出,LLM 常会漏掉source字段,导致后续步骤无源可溯。决策点 2:上下文检索与记忆调用
“上周五会议”这个时间范围,需要从历史对话或知识库中召回具体会议 ID。这里的关键不是“有没有 Memory”,而是“在什么条件下触发检索?” 我们设了硬规则:只要 query 中含时间状语(如“上周”“昨天”“第X次”),且当前 session 中无相关上下文缓存,则强制触发向量检索。否则,Agent 会盲目调用 LLM 编造会议内容——我们吃过亏,某次它编了个根本不存在的会议纪要,还同步到了生产表。决策点 3:工具选择与参数生成
检索到会议 ID 后,要调哪个工具?是get_meeting_notes还是list_all_meetings?参数怎么填?这里不能靠字符串匹配,我们用了一种轻量级的“工具签名比对”:每个 tool 的 description 都按固定模板写,比如get_meeting_notes(meeting_id: str) -> List[Dict],LLM 的输出必须严格匹配这个 signature 格式。一旦参数类型或数量不对,就拒绝执行,而不是传空值过去——后者会导致下游服务报 500,而 Agent 还以为成功了。
后面四个决策点同样关键:执行后的结果校验(决策点 4)、失败时的重试策略(决策点 5)、多步任务中的状态持久化(决策点 6)、以及当用户插入新指令(如“等等,先别同步,改成发邮件”)时的流程中断与重规划(决策点 7)。这七个点,每一个都对应一个明确的 if-else 判断、一个具体的输入输出契约、一个可监控的埋点指标。所谓“工程实现”,就是把这七个决策点,从模糊的“应该做”变成确定的“必须做、怎么做、做错了怎么办”。下面我就按这七个点,逐个拆解我们在 Rust 和 Python 双栈中落地的具体方案。
2.1 决策点 1:意图识别与任务拆解——为什么不用 RAG,而用结构化 LLM 输出?
意图识别常被简化为“分类”,但实际远不止于此。比如用户说“帮我看看张三的报销单批到哪了”,表面是查询,但背后可能隐含“如果没批,催一下财务”。如果只分出“query”类,Agent 就只会查状态,不会主动发起催办。我们的解法是:让 LLM 不仅输出意图标签,更要输出带约束的结构化动作树。
我们定义了一个最小可行 schema:
{ "intent": "query|action|info_gathering", "required_tools": ["get_reimbursement_status", "send_reminder"], "decision_rules": [ {"condition": "status == 'pending'", "action": "send_reminder"}, {"condition": "status == 'rejected'", "action": "ask_reason"} ], "output_format": "text|json|table" }关键在于decision_rules字段——它把业务逻辑从代码里抽出来,交由 LLM 基于当前 query 动态生成。这样做的好处是:当财务流程变更(比如新增“加急审批”状态),只需更新 few-shot 示例,不用改一行代码。我们用的是 Llama3-8B-Instruct,prompt 模板里强制要求输出 valid JSON,并用 Pydantic 模型做解析校验。如果解析失败,就走 fallback:用规则引擎兜底(比如含“催”字就调send_reminder)。实测下来,结构化输出成功率 92.3%,比纯文本分类高 17 个百分点,因为 LLM 更擅长填空而非自由发挥。
提示:不要用
json.loads()直接解析 LLM 输出。我们封装了一个safe_json_loads()函数,它会先用正则提取最外层{},再尝试解析,失败时返回默认空 dict 并记录告警。曾有一次 LLM 在 JSON 末尾多打了个逗号,json.loads()直接 crash,整个请求链路中断——这种细节,文档里永远不会写,但线上每小时可能撞上一次。
2.2 决策点 2:上下文检索与记忆调用——向量库不是万能的,时间戳才是记忆的锚点
Memory 常被等同于“向量数据库”,但这是最大误区。向量检索解决的是“语义相似”,而真实业务中,90% 的记忆需求是“时间精确”或“ID 精确”。比如“查我昨天下午三点的会议”,向量搜索可能召回“上周三的项目复盘”,因为语义相近(都是会议),但时间完全错位。我们的方案是:Memory 分三层:ID 索引层(主键直查)、时间索引层(时间范围扫描)、向量索引层(语义模糊匹配),且严格按优先级降序调用。
具体实现:
- ID 层:所有会议、订单、工单都生成唯一 ID(如
meet_20240520_1500_zhangsan),存 Redis Hash。Query 含 ID 特征(如“会议ID:meet_20240520”)时,直查 O(1)。 - 时间层:用 TimescaleDB 存时间序列数据。Query 含时间词时,先转成时间范围(“昨天”→
2024-05-19 00:00:00to2024-05-19 23:59:59),再 SQL 查询。我们写了专用 parser,能处理“上个月第三周”“最近 5 次”等复杂表达。 - 向量层:仅当前两层无结果时启用,且限制 top_k=3,避免噪声干扰。
这个设计让平均检索延迟从 850ms 降到 120ms。更重要的是,它解决了“记忆幻觉”问题:当用户问“我上次提的需求是什么”,Agent 不会从向量库中拼凑一个似是而非的答案,而是老老实实返回“未找到匹配记录”。工程上,Memory 的价值不在于“记住多少”,而在于“知道什么时候该承认自己忘了”。
2.3 决策点 3:工具选择与参数生成——签名比对比自然语言描述可靠十倍
Tool Use 是 Agent 最脆弱的环节。常见错误是:LLM 把search_user_by_email("zhang@xxx.com")错写成search_user_by_email(email="zhang@xxx.com", limit=10),多了一个参数,下游服务直接 400。或者把create_ticket(title, desc)的desc参数漏掉,导致工单标题是乱码。
我们的对策是:放弃自然语言描述 tool,改用机器可读的 signature。每个 tool 在注册时,必须提供:
Tool { name: "search_user_by_email", signature: "search_user_by_email(email: str) -> User", description: "根据邮箱查找用户,返回用户基本信息", examples: vec![ ("zhang@xxx.com", r#"{"name": "张三", "dept": "研发"}"#) ] }LLM 的 prompt 里明确要求:“请严格按以下 signature 格式输出 tool call:tool_name(arg1=value1, arg2=value2)。参数名、类型、顺序必须完全一致。” 我们用正则预处理 LLM 输出,提取tool_name(...)部分,再用 AST 解析器校验参数。如果email类型不是 string,或少了必填参数,就拒绝执行,返回 error message 给 LLM 让它重试。这套机制让 tool 调用失败率从 34% 降到 5.2%。最关键的是,它让调试变得极其简单:日志里直接看到expected: email:str, got: email:int,而不是翻三天日志找哪句 prompt 写错了。
注意:不要在 signature 里写太复杂的类型,比如
List[Dict[str, Any]]。LLM 解析容易出错。我们约定:所有参数类型只用基础类型(str, int, bool, float),复杂结构用 JSON string 传,由 tool 内部解析。比如create_ticket(title: str, metadata: str),metadata是个 JSON 字符串。
2.4 决策点 4:执行结果校验——没有校验的执行,等于没执行
Execution 常被当作“调用 API 就完事”,但真实世界里,API 返回的 status code 和 body 充满陷阱。比如:
- 支付接口返回 200,但 body 里
{"success": false, "msg": "余额不足"}; - CRM 接口返回 201,但实际创建的是草稿状态,需二次提交;
- 文件上传接口返回 200,但 CDN 回源要 3 秒,立刻查元数据会 404。
我们的校验分三级:
- HTTP 层:只认 2xx,其他全标为失败(不接受 302 重定向、429 限流等“软失败”);
- 业务层:每个 tool 必须定义
is_success(response: dict) -> bool方法。比如支付 tool 的校验逻辑是response.get("code") == 0 and response.get("data", {}).get("paid") is True; - 状态层:对异步操作,追加轮询校验。比如发邮件后,不是等 SMTP 返回就结束,而是调用
check_email_status(mail_id),直到返回"delivered"才算成功。
这套校验让“假成功”率归零。曾经有个 Agent 在测试环境总显示“已发送邮件”,结果生产环境里因 SMTP 配置错误,邮件全进了垃圾箱——因为没做状态层校验。现在,每个 tool 的校验逻辑都写在单元测试里,覆盖率 100%。
2.5 决策点 5:失败重试与降级——重试不是越多越好,而是越精准越好
Retry 常被滥用。有人设max_retries=5,结果一次网络抖动,Agent 连续重试 5 次,把下游服务打挂。我们的原则是:重试必须有明确的失败原因分类,且每次重试策略不同。
我们定义了四类失败:
- Transient(瞬时):如 HTTP 503、数据库连接超时。策略:指数退避(100ms, 300ms, 900ms),最多 3 次;
- Validation(校验):如参数格式错误、token 过期。策略:0 次重试,立即返回 error 给 LLM 修正;
- Business(业务):如库存不足、权限不够。策略:1 次重试,但换参数(如把
quantity=10改成quantity=5); - Fatal(致命):如 tool 未注册、schema 不匹配。策略:0 次重试,直接 abort 整个 task。
这个分类不是靠猜,而是在 tool 执行后,用统一 error handler 解析 response。比如所有requests.exceptions.Timeout归为 Transient,所有pydantic.ValidationError归为 Validation。实践下来,瞬时错误重试成功率 99.2%,业务错误通过参数调整解决率 63%,而致命错误从不浪费资源重试。
2.6 决策点 6:状态持久化与断点续传——Agent 不是无状态函数,而是有记忆的进程
Orchestration 常被误解为“流程编排”,但核心其实是“状态管理”。一个 5 步的 Agent 流程(查订单→验库存→锁库存→生成支付单→发短信),如果第三步失败,重启后不该从头开始,而应从第三步继续。我们用 Redis Stream 实现断点续传:
- 每个 task 生成唯一
task_id,作为 stream key; - 每步执行前,写入
{"step": 3, "input": {...}, "timestamp": ...}; - 每步成功后,写入
{"step": 3, "output": {...}, "status": "success"}; - Agent 启动时,先查 stream 最后一条
status: success的记录,从下一步开始。
这样,即使 Agent 进程崩溃,恢复后也能精准续跑。我们甚至支持手动干预:运维在后台删掉某条失败记录,Agent 就会自动重跑那一步。状态持久化不是为了“高可用”,而是为了“可追溯”——每一笔业务操作,都能在 Redis 里找到完整的 step-by-step 日志,这对金融、医疗等强合规场景是刚需。
2.7 决策点 7:动态重规划与中断处理——用户随时可能喊停,Agent 必须听得懂“等等”
最后一个决策点最易被忽视:当用户在 Agent 执行中插入新指令,比如“等等,别发短信,先打电话确认”,Agent 不能无视,也不能粗暴中断。我们的方案是:引入“指令优先级队列”和“可中断执行器”。
- 所有用户输入进入一个 priority queue,实时指令(含“等等”“取消”“改成”)优先级最高;
- 执行器每步完成后,检查 queue 是否有高优指令;
- 如果有,暂停当前 task,保存现场(用前面的 Redis Stream),启动新 task;
- 原 task 进入“待唤醒”状态,可被后续指令 resume 或 abort。
这个设计让 Agent 具备了真实的人机协作感。测试时,用户故意在支付单生成后说“改成微信支付”,Agent 立即中止短信发送,调用微信支付接口,全程无卡顿。技术上,我们用 Tokio 的select!宏监听多个 channel,确保指令响应延迟 < 50ms。真正的智能,不在于多快,而在于多“听话”——它得明白,用户的“等等”不是 bug,而是 feature。
3. 七个决策点如何落地?从 Rust 高性能内核到 Python 快速原型的双栈实践
光讲决策点不够,你得知道怎么把它变成代码。我们团队用双栈开发:Rust 写核心 runtime(追求极致性能与可靠性),Python 写业务 logic(追求快速迭代与生态兼容)。这不是为了炫技,而是工程上的必然选择——就像操作系统内核用 C,应用软件用 Python 一样。
3.1 Rust 内核:用 Actor 模型固化七个决策点
Rust 版本是我们 Agent 的心脏,它不碰业务,只管“决策点怎么流转”。我们用tokio+actix构建 Actor 系统:
DecisionPointActor:每个决策点是一个独立 actor,接收DecisionInput,输出DecisionOutput;OrchestratorActor:协调者,按固定顺序向各 DecisionPointActor 发送消息,处理失败回调;StateStoreActor:专管 Redis Stream 读写,所有状态变更都经它手。
关键代码片段(简化版):
// 决策点 1:意图识别 #[derive(Debug)] pub struct IntentDecision { pub query: String, pub session_id: String, } impl Handler<IntentDecision> for IntentDecisionActor { type Result = ResponseActFuture<Result<IntentOutput, Error>>; fn handle(&mut self, msg: IntentDecision, ctx: &mut Context<Self>) -> Self::Result { // 调用 LLM API,解析结构化输出 let future = self.llm_client .call_intent_prompt(&msg.query) .map(|res| { match res { Ok(json) => Ok(IntentOutput::from_json(&json)?), Err(e) => Err(Error::LlmCallFailed(e)), } }); Box::pin(future) } }这个设计的好处是:每个决策点完全解耦,可单独压测、替换、监控。比如我们发现决策点 3(tool 选择)耗时波动大,就给它单独扩 3 个实例,不影响其他点。Rust 内核的吞吐量稳定在 1200 QPS(P99 < 80ms),内存占用 < 150MB,比 Python 版本高 4.7 倍。
3.2 Python 外围:用 LangGraph 实现业务逻辑的可视化编排
Python 版本负责把业务需求翻译成决策点参数。我们用 LangGraph,但不是照搬 template,而是深度定制:
# 自定义节点,注入我们的决策点逻辑 def intent_node(state: State): # 调用 Rust 内核的 intent decision point result = rust_kernel.call_decision_point("intent", state["query"]) return {"intent": result["intent"], "tools": result["required_tools"]} def tool_node(state: State): # 根据 intent 和 tools 列表,调用 Rust 的 tool selection tool_call = rust_kernel.call_decision_point("tool_selection", { "intent": state["intent"], "available_tools": get_available_tools() }) return {"tool_call": tool_call} # 构建图 workflow = StateGraph(State) workflow.add_node("intent", intent_node) workflow.add_node("tool", tool_node) workflow.add_node("execute", execute_tool) workflow.add_edge("intent", "tool") workflow.add_edge("tool", "execute") workflow.set_entry_point("intent") app = workflow.compile()LangGraph 的价值在这里体现:它把七个决策点变成了可拖拽、可版本管理、可 A/B 测试的节点。产品经理能看懂 workflow 图,开发能直接改 node 逻辑,QA 能针对单个 node 写测试用例。我们甚至做了个内部平台,业务方上传一段文字描述(如“用户投诉要自动升级为 VIP 客服”),平台自动生成 LangGraph 代码框架,开发只需补 tool 实现。
3.3 双栈协同:gRPC 是桥梁,Protobuf 是契约
Rust 和 Python 怎么通信?我们弃用了 REST,用 gRPC + Protobuf:
- 定义
decision_point.proto:
message DecisionRequest { string point_name = 1; // "intent", "tool_selection", etc. map<string, string> params = 2; } message DecisionResponse { bool success = 1; string output_json = 2; string error_message = 3; }- Rust 实现
DecisionServiceserver; - Python 用
grpcioclient 调用。
好处是:接口契约清晰、序列化高效、天然支持流式响应。一次决策点调用,gRPC 往返 < 15ms(本地网络),比 REST 快 3 倍。更重要的是,Protobuf 强类型让前后端不敢乱传字段——Python 传params["user_id"],Rust 端必须有这个字段,否则编译不过。这杜绝了“Python 传了新字段,Rust 没处理,默默忽略”的经典 bug。
3.4 性能压测实录:如何扛住 200+ QPS 并保持 P99 < 100ms
“AI Agent 怎么扛并发”是高频问题。我们的答案不是堆机器,而是精准优化每个决策点的瓶颈:
| 决策点 | 瓶颈 | 优化方案 | 效果 |
|---|---|---|---|
| 意图识别 | LLM API 延迟 | 用 vLLM 部署 Llama3,开启 PagedAttention | P99 从 1200ms → 320ms |
| 记忆检索 | 向量库 IO | TimescaleDB 分区 + 索引优化 | 查询延迟 < 50ms |
| 工具调用 | HTTP 连接池 | Rust hyper client 复用连接,max_idle=100 | 并发连接数下降 70% |
| 状态存储 | Redis 写入 | Pipeline 批量写入 Stream | QPS 提升 3.2 倍 |
压测结果(AWS c5.4xlarge, 16vCPU/32GB):
- 单节点 Rust 内核:1200 QPS,P99=78ms;
- Python + LangGraph + gRPC:220 QPS,P99=95ms;
- 混合部署(Python 前端 + Rust 后端):215 QPS,P99=92ms。
关键发现:瓶颈永远在 LLM 推理和外部 API,不在 Agent 自身。所以我们的扩容策略是:LLM 用 vLLM 水平扩展,外部 API 用 circuit breaker 限流,Agent 内核保持单节点——它足够轻量,16GB 内存能稳跑 2000 QPS。
4. 常见问题与排查技巧实录:那些文档里不会写的血泪教训
最后,分享几个我们在真实项目里撞过的墙。这些不是“可能遇到”,而是“必然遇到”,只是时间早晚。
4.1 问题:Agent 在测试环境完美,上线后疯狂循环,日志里全是retrying step 3...
现象:一个查库存的 Agent,测试时 100% 成功,上线后在特定 SKU 上无限 retry,CPU 拉满。
根因:测试用的 mock 数据里,库存总是 >0;而真实数据中,有 SKU 库存为 -1(表示预售),tool 的校验逻辑if stock < 0: raise Exception没覆盖负数场景,导致每次调用都 fail,触发重试。
排查技巧:
- 在所有 tool 的
is_success()方法里,加一行logger.debug(f"Tool {name} response: {response}"),别只记 error; - 对重试超过 3 次的 task,自动 dump 全量 input/output 到 S3,供离线分析;
- 用
pytest写边界值测试:test_stock_negative,test_stock_zero,test_stock_huge。
解决方案:修改 tool 校验逻辑,把stock < 0视为有效状态(预售),并返回{"status": "preorder"}。同时,在决策点 5 的 Business 错误分类里,增加preorder分支,引导 LLM 生成“预计发货时间”话术。
4.2 问题:用户说“把文件发给张三”,Agent 却把文件发给了“张三丰”
现象:名字模糊匹配导致误发,涉及隐私泄露风险。
根因:向量检索的top_k=5返回了相似度最高的 5 个名字,Agent 直接取第一个,没做二次确认。
排查技巧:
- 在所有涉及“人名/ID”的检索步骤后,加一道
confidence_threshold检查:如果最高相似度 < 0.85,强制 human-in-the-loop; - 用
difflib.SequenceMatcher做字符串相似度兜底,和向量结果交叉验证; - 对敏感操作(发文件、转账、删数据),默认 require explicit confirmation。
解决方案:重构决策点 2,增加“模糊匹配确认”子步骤:当检索结果相似度在 0.7~0.95 区间时,Agent 返回“检测到多位‘张三’,请选择:1. 张三(研发部) 2. 张三丰(HR)”,用户选后才执行。
4.3 问题:Agent 突然变“傻”,所有回答都带奇怪符号,如 `` 或�
现象:某天凌晨 3 点,所有 Agent 输出出现乱码,持续 2 小时。
根因:上游 LLM API 返回的 content-type 是text/plain; charset=iso-8859-1,而我们的 Python client 默认用 utf-8 解码,导致乱码。ISO-8859-1 是西欧字符集,中文全变 ``。
排查技巧:
- 在 HTTP client 层加 middleware,打印所有 response 的
content-type和前 100 字节 raw body; - 对所有外部 API 调用,强制指定
encoding="utf-8",并 catchUnicodeDecodeError; - 用
chardet库自动探测编码,但仅作 fallback,不依赖。
解决方案:在 Rust 内核的 LLM client 里,加 encoding auto-detect:
let encoding = chardet::detect(&body).unwrap_or("utf-8"); let text = std::str::from_utf8(&body).or_else(|_| { std::string::String::from_utf8_lossy(&body).into_owned() });4.4 问题:Agent 在 Docker 容器里跑几天后,内存泄漏,OOM kill
现象:容器内存从 500MB 慢慢涨到 3GB,然后被 kill,重启后重演。
根因:Rust 的tokio::spawn创建的 task 没正确 await,形成“幽灵 task”,持续 hold 内存。特别是 retry 逻辑里,tokio::time::sleep后没检查 task 是否已被 cancel。
排查技巧:
- 用
tokio-console实时监控 task 数量和内存分配; - 在所有
spawn后加#[cfg(debug_assertions)]断言,检查 task count; - 对长周期操作(如轮询),用
tokio::select!+cancel信号。
解决方案:重构所有 retry loop:
let mut interval = tokio::time::Duration::from_millis(100); for attempt in 0..max_retries { if let Err(e) = do_work().await { if attempt == max_retries - 1 { return Err(e); } tokio::time::sleep(interval).await; interval *= 2; // exponential backoff } else { break; } }4.5 问题:用户抱怨“Agent 总是重复问我同一个问题”,如连续三次问“您要查哪个订单?”
现象:对话状态丢失,每次都要重新输入订单号。
根因:Redis 的 memory TTL 设得太短(30 分钟),用户操作间隔稍长,session 就过期,Agent 当作新会话处理。
排查技巧:
- 在所有 state read/write 操作旁加
redis_ttl_key日志,记录 key 的剩余 TTL; - 对高频会话,用
redis.expireat()设置绝对过期时间(如now() + 24h),而非相对 TTL; - 在 Agent 启动时,检查 Redis 连接健康度,失败时 fallback 到内存 cache(仅 dev 环境)。
解决方案:Session TTL 改为 7 天,但加“活跃心跳”:每次用户输入,都redis.expireat(session_key, now() + 7*24*3600)。这样,只要用户还在用,TTL 就不断刷新。
5. 工程实现之外:Agent 的边界在哪?三个必须守住的底线
写到最后,我想说点技术之外的话。做了这么多 Agent 项目,最深刻的体会是:Agent 不是万能钥匙,它有清晰的边界,越界就会反噬。这三个底线,是我们团队写进 every project 的 SOP:
5.1 底线一:绝不让 Agent 做最终决策,只做“建议执行者”
我们曾有个客户想让 Agent 直接审批采购单。我们坚决拒绝,改为:Agent 生成《采购单风险评估报告》(含供应商历史履约率、当前库存水位、预算剩余),由人点击“批准”或“驳回”。理由很简单:责任主体必须是人,不是代码。法律上,AI 的决策无法担责;工程上,任何系统都有概率出错,而人的判断是最后一道保险。现在,我们所有 Agent 的输出,都带一句“此为 AI 辅助建议,请人工确认后执行”。
5.2 底线二:所有外部 API 调用,必须有 circuit breaker 和 fallback
Agent 依赖太多外部服务(CRM、ERP、支付网关),任何一个抖动都会让它卡死。我们强制所有 tool client 集成tokio::sync::Semaphore(限流) +tower::limit::RateLimit(熔断) +fallback(降级)。比如查库存失败时,fallback 返回“库存数据暂不可用,请稍后重试”,而不是抛异常让整个流程崩掉。稳定性不是靠“不出错”,而是靠“出错时优雅降级”。
5.3 底线三:用户数据主权,必须 100% 归属用户,Agent 只是临时处理者
我们所有项目,用户数据绝不进我们的数据库。Redis 里的 session data,TTL 到期自动删除;向量库里的 embedding,用用户专属密钥加密;LLM 的 prompt,敏感字段(手机号、身份证)在进入 LLM 前就脱敏。合同里白纸黑字:“贵司数据所有权及处置权,始终属于贵司。我方仅提供计算服务,不主张任何数据权利”。这不是道德选择,而是商业生存的底线——客户敢把核心业务交给 Agent,前提是相信你守住了这条线。
我见过太多团队,为了赶 Demo,绕过这些底线,结果项目上线三个月就被叫停。真正的工程实现,不仅是代码跑通,更是对边界、责任、信任的敬畏。Agent 的未来不在“多聪明”,而在“多可靠”——可靠到用户敢让它碰自己的钱、自己的数据、自己的决策。这条路很长,但每一步,都得踩实。