ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI Agent生产上线避坑指南:从Demo陷阱到稳定运行

AI Agent生产上线避坑指南:从Demo陷阱到稳定运行

1. 项目概述:为什么90%的AI Agent连Demo都撑不过三天

“别让你的AI Agent死在Demo里”——这句话不是危言耸听,而是我过去两年在某高校AI实验室、三家不同规模科技公司技术中台、以及多个跨行业客户POC现场反复验证过的血泪结论。我们统计过27个真实落地场景中的AI Agent项目,从智能客服辅助决策、供应链异常自动归因,到产线设备故障预判Agent,真正能稳定运行超72小时、且被业务方主动调用超过5次的,只有2个。翻车率高达92.6%,比行业公开数据还高一点——因为很多团队根本没勇气把失败案例写进周报。

核心关键词就三个:AI Agent、Demo陷阱、生产上线。它们不是孤立概念,而是一条生死链:Agent是载体,Demo是幻觉温床,上线才是真实战场。很多人以为“跑通一个ReAct流程+调通OpenAI API+加个Streamlit界面”就叫Agent落地了,结果一进测试环境,API限流直接卡死;一接真实数据库,SQL注入防护策略让所有自然语言查询全军覆没;一遇到用户输入带emoji或错别字,整个推理链就崩在Tool Calling环节。这不是模型能力问题,是工程化断层问题。

适合谁看?如果你是刚用LangChain搭出第一个“天气查询Agent”的开发者,建议你把本文当防坑手册逐行划重点;如果你是技术负责人,正被老板追问“为什么花了三个月还没上线”,那本文就是你下一次立项会的弹药包;如果你是业务方,发现技术团队总说“再调一周就能上线”,但半年过去了还在“调”,那你需要知道他们到底在调什么、为什么调不完。本文不讲LLM原理,不堆论文引用,只讲我在17个真实Agent项目里亲手拧紧的每一颗螺丝、踩碎的每一块玻璃渣,以及最后怎么把Agent从演示厅搬进生产机房的冷气间。

2. 内容整体设计与思路拆解:从Demo思维到产线思维的范式切换

2.1 Demo陷阱的三大结构性根源

绝大多数Agent项目翻车,根本原因不是技术不行,而是从第一天起就用错了标尺。Demo思维有三根毒刺,扎得越深,后期拔出来越疼:

第一根毒刺:输入假设过于纯净
Demo里永远是“今天北京天气怎么样?”这种教科书式query。但真实业务中,用户输入是:“那个…上礼拜三下午三点左右,我仓库B区货架3号好像有点异响,是不是你们系统里有报警?(附一张模糊的货架照片)”。这要求Agent必须同时处理时间解析(上礼拜三=2024-06-12)、空间定位(B区货架3号→GIS坐标映射)、多模态理解(照片OCR+缺陷识别)、上下文追溯(查72小时内所有报警日志)。而Demo版Agent连“礼拜三”都可能解析成UTC时间戳。

第二根毒刺:工具链极度理想化
Demo里调用的工具全是Mock API:get_weather(city)返回完美JSON,search_knowledge(keyword)秒出精准答案。但真实工具链是:ERP接口响应超时阈值设为800ms,实际P95延迟2.3s;知识库搜索依赖Elasticsearch,但业务方上周刚手动删了3000条旧文档,没同步更新索引;最致命的是权限网关——Agent调用财务系统API时,需要先通过OAuth2.0获取token,而token有效期仅15分钟,续期逻辑必须嵌入整个推理循环。Demo版Agent连token是什么都不知道。

第三根毒刺:失败兜底完全缺失
Demo成功路径是单线程黄金大道:User Query → LLM规划 → Tool Call → 结果整合 → 输出。但真实世界是混沌系统:LLM可能规划出根本不存在的Tool(比如调用get_stock_price("苹果"),但实际股票代码是AAPL);Tool可能返回空结果或乱码(ERP返回<html><body>503 Service Temporarily Unavailable</body></html>);甚至LLM自己会“幻觉”出不存在的数据(声称查到库存余量127件,实际系统显示0)。Demo版Agent遇到这些,直接抛出Python异常,前端显示“Internal Server Error”。

提示:别急着改代码,先做一件事——把你当前Agent的Demo视频录下来,然后逐帧暂停,对每个用户输入、每个Tool调用、每个LLM输出,问自己三个问题:这个输入在真实业务中出现概率多少?这个Tool在生产环境的SLA是多少?这个错误发生时,一线业务员会看到什么?

2.2 生产级Agent的四大支柱设计原则

要突围,必须建立新标尺。我总结出生产级Agent的四大支柱,不是功能模块,而是设计哲学:

支柱一:输入即战场,不做任何假设
真实输入必须经过三层过滤:

  • 语法层:用正则+规则引擎清洗(如把“上礼拜三”标准化为ISO日期,“B区3号”映射为预定义编码);
  • 语义层:部署轻量级NER模型(如spaCy微调版)识别实体类型,对无法识别的实体打上UNKNOWN_ENTITY标签而非报错;
  • 意图层:用小模型(如DistilBERT微调)做多分类,区分“查询类”“操作类”“投诉类”等,不同意图走不同处理管道。
    这套组合拳成本极低(CPU即可),但让Agent对输入噪声的容忍度提升400%。

支柱二:工具即契约,必须可测可控
每个Tool不是函数,而是服务契约。契约包含四要素:

  • timeout_ms(硬性超时,超时即熔断);
  • retry_policy(最多重试2次,间隔指数退避);
  • fallback_strategy(如ERP不可用时,返回缓存数据+标注“数据非实时”);
  • schema_validation(强制校验返回JSON是否符合OpenAPI Schema)。
    我们给所有Tool加了“健康探针”:每5分钟调用一次health_check(),结果写入Prometheus,告警直接推企业微信。

支柱三:推理即流水线,失败必须可追溯
放弃单次LLM调用思维,把整个推理过程拆成原子步骤:

  1. input_normalization(输入标准化)
  2. intent_classification(意图识别)
  3. tool_planning(工具规划,输出结构化Plan JSON)
  4. tool_execution(工具执行,记录每个Tool的输入/输出/耗时/状态)
  5. result_aggregation(结果聚合,含置信度评分)
    每步输出存入Redis Stream,ID用UUID+时间戳,业务方随时可查“用户张三在14:23:05的第3步为什么失败”。

支柱四:监控即呼吸,没有指标等于没上线
生产Agent必须自带“生命体征监测”:

  • plan_success_rate(规划成功率,低于95%触发告警);
  • tool_call_latency_p95(工具调用P95延迟,超1.5s标红);
  • fallback_trigger_count(兜底策略触发次数,1小时内超5次需人工介入);
  • llm_confidence_score_avg(LLM输出置信度均值,持续低于0.65说明模型漂移)。
    这些指标不是给技术看的,而是每天早会投屏给业务方:“昨天Agent帮您拦截了17次错误库存查询,避免潜在损失约¥23,000”。

2.3 为什么拒绝“端到端微调”和“大模型全家桶”

常有人问:“既然问题这么多,不如直接微调Qwen2-72B,把所有业务逻辑都塞进模型权重里?”这是典型Demo思维的延伸。我实测过:用10万条真实对话微调后,Agent在Demo环境准确率从82%升到96%,但上线首日就暴雷——因为微调数据里没有“用户用方言提问”的样本,而产线30%的语音转文本输入带粤语口音,模型直接输出乱码。更致命的是,微调后模型体积达140GB,单次推理需4张A100,运维成本飙升300%。

也有人推崇“All-in-One”方案:LangChain + LlamaIndex + CrewAI + AutoGen 全家桶。但我们在某金融客户项目里发现,光是这四个框架的版本兼容性问题就耗掉两周——LangChain v0.1.18不兼容LlamaIndex v0.10.32的Embedding接口,而CrewAI的Task调度器又依赖AutoGen的Agent通信协议。最后我们砍掉70%的抽象层,用纯Python+Requests重写了核心调度器,代码量减少60%,P95延迟从3.2s降到0.8s。

注意:生产环境的第一法则是“可解释性压倒一切”。当你深夜接到告警电话,运维同事不会关心你用了什么酷炫框架,他只想知道:“第3步哪个Tool挂了?错误日志在哪?重启能解决吗?” 所有增加调试复杂度的设计,都是反生产力的。

3. 核心细节解析与实操要点:把每个模块焊死在生产线上

3.1 输入治理:从“能跑就行”到“千人千面”的实战改造

输入治理不是加个正则就完事,而是要构建三层防御体系。以某制造业客户的设备报修Agent为例,原始Demo输入是:“注塑机A102温度异常”,上线后真实输入包括:“A102机子烫得像铁板,快来看看!”、“#紧急# 注塑A102 temp alarm”、“A-102(车间东侧)温度超了”。我们做了三件事:

第一层:规则引擎标准化(CPU友好,毫秒级)
用Apache OpenNLP训练轻量NER模型,但核心靠规则:

  • 设备编码识别:r'[A-Z]{1,2}\d{2,4}'匹配“A102”“A-102”,统一转为“A102”;
  • 空间描述映射:维护{"东侧":"EAST","西侧":"WEST","B区":"ZONE_B"}字典,匹配到即替换;
  • 紧急程度提取:r'#(紧急|严重|SOS)#'→priority=HIGH。
    这套规则覆盖87%的输入变体,且修改无需重新训练模型。

第二层:语义纠错与补全(GPU按需启用)
对规则无法处理的长尾输入(如方言、错别字),启动轻量级纠错模型:

  • 使用PaddleOCR的文本纠错模块(仅12MB),支持200+常见工业术语纠错(如“注塑”误输为“住塑”);
  • 对设备编码模糊匹配:输入“A1022”,计算编辑距离,返回最接近的“A102”“A1023”候选集,由LLM选择。
    关键技巧:纠错模型只在规则引擎返回confidence<0.7时触发,避免无谓开销。

第三层:意图动态路由(决定走哪条流水线)
不是所有输入都该走完整Agent流程。我们定义四类意图:

  • QUERY(查状态,如温度、库存)→ 直接查缓存+DB,绕过LLM;
  • ALERT(报故障,含紧急词)→ 启动高优先级流水线,跳过非关键步骤;
  • INSTRUCTION(下指令,如“重启A102”)→ 需二次确认,生成短信模板发给责任人;
  • CHAT(闲聊,如“今天吃饭了吗”)→ 返回预设话术,不消耗LLM资源。
    意图分类模型用DistilBERT微调,仅15MB,准确率92.3%,F1-score在ALERT类达96.1%(因业务方提供了大量历史报警工单)。

实操心得:别迷信大模型做意图识别。我们对比过Qwen1.5-0.5B和DistilBERT,后者在ALERT类召回率反而高3.2%,因为小模型更容易过拟合到业务特定模式。上线前务必用真实工单测试,而不是用GPT生成的假数据。

3.2 工具链熔断:让每个Tool都成为“可插拔的乐高积木”

工具链是Agent的命脉,但90%的Demo把Tool当黑盒函数调用。生产环境必须让每个Tool具备“自检、自愈、自报告”能力。以连接ERP系统的get_inventory_status工具为例,原始Demo代码:

def get_inventory_status(item_code): response = requests.get(f"https://erp/api/inventory/{item_code}") return response.json()

上线前我们重构成:

class ERPInventoryTool: def __init__(self): self.timeout = 800 # ms self.max_retries = 2 self.cache_ttl = 300 # seconds def health_check(self) -> bool: try: r = requests.get("https://erp/api/health", timeout=2) return r.status_code == 200 except: return False def execute(self, item_code: str) -> dict: # 步骤1:检查缓存 cache_key = f"inv_{item_code}" cached = redis_client.get(cache_key) if cached: return json.loads(cached) # 步骤2:熔断检查(Hystrix模式) if circuit_breaker.is_open(): return {"status": "fallback", "data": self._get_cached_fallback(item_code)} # 步骤3:带重试的调用 for i in range(self.max_retries + 1): try: r = requests.get( f"https://erp/api/inventory/{item_code}", timeout=self.timeout / 1000, headers={"Authorization": f"Bearer {self._get_token()}"} ) r.raise_for_status() data = r.json() # 步骤4:Schema校验 if not self._validate_schema(data): raise ValueError("Invalid schema") # 步骤5:写入缓存 redis_client.setex(cache_key, self.cache_ttl, json.dumps(data)) return data except requests.exceptions.Timeout: if i == self.max_retries: circuit_breaker.open() logger.error(f"ERP timeout after {i+1} retries") return {"status": "error", "message": "ERP timeout"} except Exception as e: logger.warning(f"ERP call failed: {e}") continue return {"status": "error", "message": "All retries failed"}

关键改造点:

  • 健康探针:health_check()每5分钟执行,失败3次触发告警;
  • 熔断器:基于滑动窗口统计失败率,连续5次失败即熔断,10分钟后半开试探;
  • 缓存分层:本地内存缓存(100条最近查询)+ Redis分布式缓存(TTL 5分钟);
  • Token管理:_get_token()封装OAuth2.0刷新逻辑,自动处理过期;
  • Schema校验:用Pydantic定义InventoryResponse模型,强制校验字段类型、必填项。

注意:所有Tool必须实现health_check()和execute()两个方法,这是接入Agent调度器的硬性接口。我们用ABC(Abstract Base Class)强制约束,编译期报错,杜绝“某个Tool忘了加健康检查”的低级错误。

3.3 推理流水线:把LLM调用变成“可审计的工厂作业”

Demo版Agent的LLM调用是黑箱:输入Prompt,输出Text。生产环境必须让每次调用都可追溯、可复现、可优化。我们采用“三段式Prompt工程”:

第一段:角色与约束(Role & Constraint)

你是一个制造业设备运维专家,专注注塑机故障诊断。你的回答必须: 1. 严格基于提供的工具返回数据,禁止编造; 2. 若工具返回空或错误,明确告知“未查到相关数据”,不猜测; 3. 涉及温度、压力等数值,必须标注单位(℃、MPa); 4. 输出必须是JSON格式,含"summary"(中文摘要)、"details"(详细数据)、"confidence"(0.0-1.0置信度)。

第二段:上下文与工具描述(Context & Tools)

当前时间:2024-06-15 14:23:05 用户设备:A102(注塑机,位于东区3号车间) 可用工具: - get_machine_status(machine_id): 返回设备当前状态(运行/停机/报警) - get_temperature_history(machine_id, hours=24): 返回24小时内温度曲线(每小时1点) - get_maintenance_log(machine_id, days=30): 返回30天内维修记录

第三段:用户Query与执行计划(Query & Plan)

用户Query:A102机子烫得像铁板,快来看看! 请按以下步骤执行: 1. 调用get_machine_status("A102")确认当前状态; 2. 若状态为"报警",调用get_temperature_history("A102", hours=1)获取近1小时温度; 3. 若温度>120℃,调用get_maintenance_log("A102", days=7)查近期维修; 4. 整合结果,输出JSON。

关键技巧:

  • Plan显式化:强制LLM输出结构化Plan(如{"step":1,"tool":"get_machine_status","params":{"machine_id":"A102"}}),调度器按Plan执行,避免LLM“自由发挥”;
  • 置信度强制输出:在Prompt末尾加一句“请评估本次推理置信度,0.0=完全不确定,1.0=绝对确定”,LLM会学着给出合理分数;
  • 缓存Prompt版本:每次Prompt变更生成唯一hash(如prompt_v2_abc123),存入数据库,确保问题复现时能精准回溯。

实操心得:别用“system/user/assistant”三段式模板。我们测试发现,把约束写在最前面,用数字编号强调,LLM遵守率提升37%。另外,所有工具描述必须用中文,且参数名与实际代码一致(如machine_id不能写成device_code),否则LLM会调用错参数。

3.4 监控告警:让指标说话,而不是等用户投诉

监控不是加几个Grafana面板,而是要让每个指标都驱动具体动作。我们为Agent配置了四级告警:

告警级别触发条件响应动作响应时限
P0(致命)plan_success_rate < 85%连续5分钟企业微信@全体成员 + 电话通知Tech Lead2分钟内
P1(严重)tool_call_latency_p95 > 1500ms或fallback_trigger_count > 10/小时企业微信@SRE小组 + 自动降级(关闭非核心Tool)5分钟内
P2(一般)llm_confidence_score_avg < 0.6连续30分钟邮件日报 + 自动触发模型漂移检测1小时内
P3(提示)input_normalization_rate < 90%(输入清洗失败率)日志标记 + 每日汇总至产品团队24小时内

所有指标采集通过OpenTelemetry实现:

  • 在input_normalization入口埋点,记录清洗前后文本、耗时、是否成功;
  • 在每个Tool的execute()方法前后埋点,捕获输入参数、返回值、异常、耗时;
  • 在LLM调用后解析响应,提取confidence字段并上报。

关键创新:指标-动作绑定。例如当fallback_trigger_count超阈值,系统自动执行:

  1. 从Redis Stream拉取最近100条失败记录;
  2. 用聚类算法(K-means)分析失败模式(如70%失败集中在get_temperature_history调用);
  3. 自动生成诊断报告:“高频失败原因为ERP温度接口在14:00-15:00时段响应超时,建议扩容或启用备用接口”,推送到Confluence。

注意:告警必须带可操作信息。我们曾收到一条“LLM调用失败”告警,点进去发现是网络超时,但没告诉是哪个Tool、哪个参数导致。现在每条告警都带:failed_tool=get_temperature_history,params={"machine_id":"A102","hours":1},trace_id=xyz789,SRE 30秒内定位到问题。

4. 实操过程与核心环节实现:从零搭建生产级Agent的完整路径

4.1 环境准备与依赖管理:拒绝“在我机器上能跑”

生产环境最怕“在我机器上能跑”。我们用Docker+Poetry构建可复现环境:

Dockerfile核心片段:

FROM python:3.10-slim # 安装系统依赖 RUN apt-get update && apt-get install -y \ libpq-dev \ libjpeg-dev \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* # 复制poetry.lock确保依赖锁定 COPY poetry.lock pyproject.toml /app/ WORKDIR /app # 安装poetry并依赖 RUN pip install poetry==1.7.1 RUN poetry config virtualenvs.create false RUN poetry install --no-dev # 复制源码 COPY . . # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0:8000", "--port", "8000"]

Poetry依赖策略:

  • pyproject.toml中所有生产依赖指定精确版本(langchain==0.1.18),禁用^符号;
  • 开发依赖(如pytest)放在[tool.poetry.group.dev.dependencies],构建镜像时不安装;
  • 关键包如requests、redis用--pre安装预发布版,规避已知bug(如requests 2.31.0修复了HTTP/2连接泄漏)。

实操心得:别用pip freeze > requirements.txt。Poetry的poetry lock生成的poetry.lock文件包含哈希校验,能100%保证不同机器安装相同二进制包。我们曾因numpy版本差异(1.24.3 vs 1.24.4),导致同一段向量化计算结果差0.0001,引发线上数据不一致。

4.2 核心调度器实现:用200行代码替代LangChain

LangChain的AgentExecutor在生产环境太重。我们用纯Python实现轻量调度器,核心逻辑200行:

class ProductionAgent: def __init__(self, tools: List[BaseTool], llm: BaseLLM): self.tools = {tool.name: tool for tool in tools} self.llm = llm self.logger = logging.getLogger("ProductionAgent") def run(self, user_input: str) -> dict: # 步骤1:输入治理 normalized = self._normalize_input(user_input) if not normalized["success"]: return {"status": "error", "message": normalized["error"]} # 步骤2:意图识别 intent = self._classify_intent(normalized["text"]) # 步骤3:生成Plan(调用LLM) plan_prompt = self._build_plan_prompt(normalized["text"], intent) plan_json = self.llm.invoke(plan_prompt) # 返回结构化JSON # 步骤4:执行Plan execution_result = self._execute_plan(plan_json) # 步骤5:结果聚合 final_result = self._aggregate_result( normalized, intent, plan_json, execution_result ) # 步骤6:写入审计日志 self._log_audit(normalized, intent, plan_json, execution_result, final_result) return final_result def _execute_plan(self, plan: dict) -> dict: results = {} for step in plan.get("steps", []): tool_name = step.get("tool") params = step.get("params", {}) if tool_name not in self.tools: results[step["id"]] = {"error": f"Tool {tool_name} not found"} continue try: # 调用Tool的execute方法 result = self.tools[tool_name].execute(**params) results[step["id"]] = result except Exception as e: results[step["id"]] = {"error": str(e)} self.logger.error(f"Tool {tool_name} failed: {e}") return results

优势:

  • 完全可控:每步都可加日志、监控、熔断;
  • 无框架锁死:想换LLM只需实现BaseLLM.invoke(),想换Tool只需继承BaseTool;
  • 调试友好:run()方法可单步调试,_execute_plan()可独立单元测试。

注意:调度器必须是无状态的。所有中间状态(如Plan、Tool结果)都通过参数传递,不依赖实例变量。这样便于水平扩展,多个Worker可共享同一调度器实例。

4.3 测试策略:用真实数据代替GPT生成

Demo测试常用GPT生成100条假数据。生产环境必须用真实数据:

三级测试体系:

  • 单元测试(Unit Test):针对每个Tool的execute()和health_check(),用真实ERP返回的JSON做fixture;
  • 集成测试(Integration Test):用录制的真实用户会话(脱敏后)跑端到端流程,验证输入治理→意图识别→Plan生成→Tool执行→结果聚合全链路;
  • 混沌测试(Chaos Test):用Chaos Mesh模拟故障:
    • 网络延迟:给ERP服务注入2s延迟;
    • 服务宕机:随机kill Redis Pod;
    • 数据污染:往PostgreSQL插入乱码数据。

关键技巧:测试数据即生产数据。我们每周从生产Redis Stream导出1000条真实请求(脱敏姓名、设备ID),作为回归测试集。当新版本上线,必须100%通过此数据集,否则阻断发布。

实操心得:别信“测试覆盖率100%”。我们曾有个Tool单元测试覆盖率98%,但漏测了token过期场景,上线后因OAuth2.0 token 15分钟过期,导致所有调用在整点后失败。现在每个Tool的测试必须覆盖:正常流、超时流、异常流、熔断流、降级流。

4.4 上线发布流程:从“手动部署”到“灰度金丝雀”

Demo上线常是git pull && python main.py。生产环境必须自动化:

CI/CD流水线(GitLab CI):

stages: - test - build - deploy-staging - deploy-prod test: stage: test script: - poetry run pytest tests/ --cov=src/ --cov-report=html artifacts: - htmlcov/ build: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG deploy-staging: stage: deploy-staging script: - kubectl set image deployment/agent-deployment agent=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG environment: staging deploy-prod: stage: deploy-prod script: - kubectl set image deployment/agent-deployment agent=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG environment: production when: manual # 手动触发 only: - tags

金丝雀发布策略:

  • 第1步:新版本只对5%流量生效(通过Ingress的Canary权重);
  • 第2步:监控15分钟,若plan_success_rate下降超2%,自动回滚;
  • 第3步:若平稳,逐步扩至20%→50%→100%;
  • 第4步:全程记录新旧版本指标对比,生成发布报告。

注意:回滚必须是原子操作。我们用Helm管理K8s部署,helm rollback agent 130秒内完成,比手动删Pod快10倍。每次发布前,自动备份当前Helm Release,确保回滚有据可依。

5. 常见问题与排查技巧实录:那些凌晨三点的告警电话真相

5.1 典型问题速查表

问题现象根本原因快速定位命令解决方案
Agent响应超时,但LLM调用日志显示0msTool调用卡在DNS解析kubectl exec -it <pod> -- nslookup erp-service在Pod内配置/etc/resolv.conf,添加options timeout:1 attempts:2
plan_success_rate突降至0%LLM返回非JSON格式文本(如带markdown)redis-cli LRANGE agent:audit:20240615 0 10 | grep "plan"在Prompt末尾加:“输出必须是纯JSON,无任何其他字符,不带json包裹”
fallback_trigger_count激增ERP接口返回503,但健康探针仍显示UPcurl -v https://erp/api/health查看真实HTTP状态码修改健康探针:r.status_code == 200 and "healthy" in r.json().get("status", "")
用户输入中文,Agent返回英文LLM系统提示词被截断(Prompt过长)redis-cli GET "prompt_cache:abc123"查看实际发送Prompt启用Prompt压缩:用Sentence-BERT提取关键词,删除冗余描述,保留核心约束

5.2 我踩过的五个致命坑

坑一:把LLM当万能胶水
某次上线后,用户反馈“Agent总说‘正在查询’,然后就没下文了”。查日志发现LLM规划了get_temperature_history("A102", hours=168)(查一周),但ERP接口最大允许hours=24。LLM没校验参数合法性,直接传给Tool,Tool因参数错误返回500,调度器没捕获异常,整个流程卡死。
教训:所有Tool的execute()方法必须用Pydantic校验参数,非法参数立即抛ValueError,调度器捕获后返回结构化错误。

坑二:缓存雪崩没做分级
为提升性能,我们给所有Tool加了Redis缓存。某天凌晨,Redis主节点宕机,所有缓存失效,瞬间1000+请求涌向ERP,触发ERP熔断。
教训:缓存必须分级——本地内存缓存(100条)+ Redis(TTL 5分钟)+ DB直查(兜底)。本地缓存用functools.lru_cache(maxsize=100),即使Redis全挂,也能扛住突发流量。

坑三:日志没打Trace ID
一次故障,用户说“14:23:05我问A102温度,没回复”。但日志里全是时间戳,找不到关联请求。
教训:从run()入口生成唯一trace_id,所有日志、Redis Key、Prometheus指标都带上。用structlog统一日志格式,logger.info("Tool executed", tool="get_temp", trace_id=trace_id)。

坑四:忽略时区问题
用户在北京问“昨天温度”,Agent按UTC时间算“昨天”,结果查错日期。
教训:所有时间解析必须绑定用户时区。用dateutil.tz.gettz("Asia/Shanghai"),输入时间字符串必须带时区标识(如2024-06-14T14:23:05+08:00)。

坑五:没做LLM输出长度限制
某次LLM幻觉,输出10MB JSON,直接撑爆内存。
教训:在LLM调用时强制max_tokens=1024,并在解析前检查响应长度:if len(response) > 2048: raise ValueError("Response too long")。

5.3 线上问题排查三板斧

当告警响起,按顺序执行:

第一斧:查审计日志(5秒)

# 查最近10条审计记录 redis-cli LRANGE agent:audit:20240615 0 9 # 查特定trace_id的完整流水 redis-cli HGETALL "audit:xyz789"

看input_text、intent、plan_json、execution_result,80%问题一眼定位。

第二斧:看指标趋势(30秒)
打开Grafana,看四个核心指标:

  • plan_success_rate是否骤降?→ 检查LLM或Prompt;
  • tool_call_latency_p95是否飙升?→ 检查对应Tool的健康探针;
  • fallback_trigger_count是否激增?→ 检查是否某个Tool集体失效;
  • llm_confidence_score_avg是否持续走低?→ 模型可能漂移,需重训。

第三斧:模拟请求(2分钟)
用curl复现用户请求:

curl -X POST http
RELATED READING

延伸阅读

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