1. 这不是概念炒作,是工程师每天要填的坑
“AI Agent”这个词现在满天飞,从技术大会PPT到招聘JD,再到投资人BP里,几乎成了标配词汇。但如果你真坐下来写一个能跑通的Agent,很快就会发现:它既不像LLM那样调个API就能出结果,也不像传统微服务那样有清晰的边界和契约。它更像一个在混沌中不断做决策、试错、回滚、重试的“数字实习生”——聪明,但容易迷路;强大,但需要你手把手教它怎么不撞墙。
我过去三年带过7个落地项目,从金融风控辅助决策系统,到制造业设备故障诊断助手,再到教育领域的个性化学习路径生成器,所有项目最终都绕不开Agent这个模块。而每次重构Agent层,核心矛盾从来不是“要不要用”,而是“怎么让它在真实业务流里不掉链子”。标题里说的“七要素”和“七个决策点”,不是学术论文里的漂亮框架,是我把23个失败版本的日志、监控截图、压测报告摊开后,用红笔圈出来的七个必须现场拍板、不能靠文档约定的位置。
比如“工具调用失败后是否重试”,表面看是个配置开关,实际背后牵扯着:下游API的幂等性设计是否可靠、用户等待容忍阈值是多少、当前请求是否涉及资金类操作、重试时要不要降级为人工兜底……这些根本没法写进通用框架,只能在每个具体场景里硬碰硬地权衡。再比如“记忆管理策略”,用Redis缓存对话历史?那缓存击穿时Agent会不会彻底失忆?用向量库做长期记忆?那每次检索的token开销怎么控?这些问题,没有标准答案,只有工程取舍。
所以这篇文章不讲“Agent是什么”,不画四层架构图,不罗列10个开源框架对比。我们只聚焦一件事:当你打开IDE,新建一个agent_core.py文件,光标在第一行闪烁时,你真正要面对的七个关键决策点。每一个点,我都给出当时在XX银行智能投顾项目里实测过的参数、踩过的坑、改过的三次代码逻辑,以及为什么最后选了那个看起来最笨、但上线后三个月零事故的方案。
关键词已经很清晰了:AI Agent、Agent、LLM、工具、循环机制。它们不是孤立的词,而是一条流水线上的工位编号。LLM是主操作员,工具是它手边的扳手和万用表,循环机制是传送带,而七个决策点,就是七个质检关卡——少设一个,整条线就可能产出废品。
适合谁读?如果你正在写第一个Agent原型,卡在“调用工具后不知道下一步该干嘛”;如果你的Agent在测试环境跑得飞起,一上生产就超时熔断;如果你被产品追问“为什么用户问三次才答对”,而你翻日志看到的是十层嵌套的retry log……那你不是缺理论,是缺这七个位置上的实操刻度尺。
2. 七要素不是清单,是Agent的骨骼解剖图
很多人把“七要素”当成检查清单,逐项打钩就以为完成了。这是最大的误区。七要素其实是Agent的解剖学结构——每一项都对应着真实运行时的一块肌肉、一根神经、一段血管。漏掉任何一块,Agent要么瘫痪,要么抽搐,绝不可能稳健行走。
2.1 目标(Goal):不是用户输入的复述,而是可执行的契约
目标(Goal)常被简单理解为“用户问题的重述”。但在工程实现中,它必须是带约束条件的可执行契约。例如用户说:“帮我查一下张三最近三个月的交易异常”。如果直接把这句话喂给LLM,它大概率会生成一个模糊的SQL或API调用,然后失败。
我们真正的目标契约长这样:
{ "user_id": "zhangsan_202405", "time_window": {"start": "2024-02-01", "end": "2024-04-30"}, "anomaly_types": ["large_amount", "frequent_small", "cross_region"], "output_format": "json", "max_retries": 2, "timeout_ms": 8000 }注意三个关键点:
- 实体绑定:
user_id必须从用户会话上下文中提取并校验,不能依赖LLM识别(LLM会把“张三”错认成“张山”); - 时间窗口显式化:LLM对“最近三个月”的理解偏差极大,必须由前置规则引擎固化;
- 输出契约强制:明确要求
json格式,避免LLM返回自然语言描述导致下游解析崩溃。
提示:我们在某券商项目里吃过亏。初期让LLM自己解析时间范围,结果遇到“上个月最后一个周五”这种表述,LLM生成的时间戳错了一天,导致漏查一笔关键异常交易。后来强制所有时间、金额、ID类字段走正则+规则引擎预处理,LLM只负责逻辑判断,错误率从17%降到0.3%。
2.2 记忆(Memory):不是缓存,是分层的神经突触
记忆(Memory)常被简化为“把对话存进Redis”。但真实场景中,Agent的记忆必须分层,且每层有不同刷新策略:
| 层级 | 数据类型 | 存储介质 | 刷新触发条件 | 典型容量 |
|---|---|---|---|---|
| 短期工作记忆 | 当前会话token序列 | LLM context window | 新消息到达 | 4K tokens |
| 中期上下文记忆 | 用户画像/偏好/历史交互摘要 | Redis Hash | 用户主动修改偏好 | <1MB |
| 长期知识记忆 | 行业规则/产品手册/故障案例库 | 向量数据库 | 每日增量同步 | TB级 |
关键陷阱在于层级混淆。曾有个医疗Agent项目,把患者病史(中期记忆)和最新检验报告(短期记忆)全塞进LLM context,导致context爆炸,推理延迟飙升到12秒。后来拆解后,只把检验报告原文放context,病史摘要(如“高血压病史5年,服药依从性差”)存Redis,LLM提示词里只引用摘要,延迟降到1.8秒。
注意:向量库检索不是“越快越好”。我们在某工业设备诊断Agent中发现,用HNSW索引召回Top5故障案例,准确率92%;但换成暴力检索Top20,准确率反而降到86%——因为噪声案例干扰了LLM的判断权重。最终选定Top8+Rerank策略,平衡速度与精度。
2.3 工具(Tools):不是插件列表,是带熔断的工具链
工具(Tools)绝非“注册一堆函数然后让LLM挑”。它是带熔断、降级、超时的工具链。每个工具必须声明:
schema: OpenAPI风格的JSON Schema,LLM生成参数必须严格校验;timeout_ms: 单次调用最大耗时,超时自动中断;max_retries: 允许重试次数,且每次重试需指数退避;fallback: 熔断后的降级方案(如返回缓存数据、调用轻量API、返回兜底文案)。
例如一个查询账户余额的工具:
{ "name": "get_account_balance", "description": "获取指定账户当前可用余额,单位:元", "schema": { "type": "object", "properties": { "account_id": {"type": "string", "pattern": "^ACC\\d{8}$"} }, "required": ["account_id"] }, "timeout_ms": 3000, "max_retries": 1, "fallback": {"type": "cache", "key": "balance_{account_id}"} }这里pattern校验比LLM生成更可靠;timeout_ms防止下游支付网关卡死;fallback确保即使核心系统宕机,Agent仍能返回“余额查询暂时不可用,请稍后再试”。
2.4 规划(Planning):不是思维链,是可回滚的决策树
规划(Planning)常被等同于“Chain-of-Thought”。但工程上,它必须是带回滚标记的决策树。LLM输出的每一步动作,都要附带:
step_id: 唯一标识,用于日志追踪;dependencies: 依赖的前序步骤ID(支持并行/串行);rollback_action: 若此步失败,如何回退(如删除已创建的临时订单);confidence_score: LLM自评置信度(0-1),低于阈值触发人工审核。
在电商促销Agent中,我们曾遇到LLM规划“先发券再扣库存”,但实际业务要求“先扣库存再发券”。通过在规划阶段强制注入业务规则约束("inventory_check_must_before_coupon_issue": true),并让LLM在每步输出中显式声明依赖关系,成功将流程错误率归零。
2.5 行动(Action):不是函数调用,是带审计的原子操作
行动(Action)是工具调用的执行层。它必须满足:
- 原子性:单次Action要么全成功,要么全失败,绝不允许半截状态;
- 可审计:记录完整输入、输出、耗时、错误码、调用方IP;
- 可观测:每个Action暴露Prometheus指标(
agent_action_duration_seconds{tool="xxx",status="success"} 0.234)。
我们曾用Python装饰器统一实现:
@agent_action( tool_name="update_user_profile", timeout=5.0, audit_log=True, metrics=True ) def update_user_profile(account_id: str, profile_data: dict) -> dict: # 实际业务逻辑 pass这样所有Action自动获得熔断、日志、监控能力,无需每个函数重复造轮子。
2.6 观察(Observation):不是返回值,是带语义的反馈信号
观察(Observation)是工具执行后的反馈。它不能只是原始JSON,必须结构化为:
status:"success"/"error"/"partial_success"(如批量操作部分失败);data: 业务数据(经脱敏);metadata: 耗时、trace_id、下游服务版本;suggestion: 对LLM的下一步提示(如"error_code": "RATE_LIMIT_EXCEEDED", "suggestion": "wait 60s then retry")。
在物流Agent中,当查询运单接口返回“查无此单”,我们不直接抛异常,而是返回:
{ "status": "error", "error_code": "TRACKING_NOT_FOUND", "suggestion": "ask_user_to_confirm_tracking_number" }LLM据此生成:“您提供的单号可能有误,能否再核对一下?”而非冷冰冰的“未找到运单”。
2.7 循环(Loop):不是while True,是带终止条件的状态机
循环(Loop)是Agent的生命线。它绝非简单while not done:,而是带五种终止状态的状态机:
| 状态 | 触发条件 | 处理方式 |
|---|---|---|
SUCCESS | LLM明确输出{"final_answer": "..."} | 返回结果,结束会话 |
MAX_STEPS_EXCEEDED | 步骤数 > 12(防LLM无限循环) | 强制终止,返回“问题较复杂,已转人工” |
TOOL_FAILURE_LOOP | 同一工具连续失败3次 | 切换备用工具或降级 |
CONTEXT_OVERFLOW | 当前context token > 90%上限 | 清理最旧记忆,保留关键摘要 |
USER_INTERRUPTION | 用户发送新问题或“算了” | 重置状态,开始新会话 |
我们在某政务咨询Agent中,将MAX_STEPS_EXCEEDED设为8步。实测发现,超过8步还没解决的问题,92%需要人工介入,强行让LLM继续只会降低回答质量。
3. 七个决策点:工程师键盘上的七个物理按键
七要素是Agent的“器官”,而七个决策点,是你写代码时必须按下的七个物理按键。每个键按下前,都要问自己:这个选择,在我的业务场景里,代价是什么?
3.1 决策点一:LLM选型——不是越大越好,是“够用且可控”
LLM是Agent的大脑,但选型不是比参数。关键看三个工程指标:
- 推理稳定性:同一输入多次调用,输出token分布的标准差。我们测试过Qwen2-7B和Llama3-8B,前者在金融术语生成上标准差0.03,后者达0.12——意味着后者更容易“胡言乱语”。
- 工具调用格式服从度:用相同prompt模板测试100次工具调用,能正确生成JSON Schema的比例。Qwen2-7B达98%,Llama3-8B仅82%(常漏掉逗号或引号)。
- 上下文压缩能力:当context塞满8K tokens时,对早期记忆的召回准确率。Qwen2-7B保持76%,Llama3-8B跌至41%。
最终我们在银行项目选Qwen2-7B,不是因为它最强,而是它在“稳定输出JSON”和“长文本记忆”上表现最均衡。用更大模型反而增加了调试成本——因为它的“创造力”会破坏工具调用的确定性。
实操心得:别迷信benchmark。拿你的真实业务query(如“查询2024年Q1基金赎回手续费率”)跑100次,统计:
- 成功生成有效SQL的比例
- 生成SQL中字段名拼写正确的比例
- 执行后返回结果格式符合预期的比例
三项加权平均,才是你的真实得分。
3.2 决策点二:工具编排——不是自由发挥,是带护栏的沙盒
LLM调用工具,必须戴“护栏”。我们采用三层防护:
- Schema预校验:LLM输出JSON前,用Pydantic Model强制校验,不通过直接报错;
- 参数白名单:对敏感字段(如
account_id)建立白名单库,不在库中则拒绝; - 调用频控:单个会话内,同一工具1分钟最多调用3次,防LLM陷入死循环。
曾有个Agent因LLM反复调用“天气查询”工具(因用户问“今天适合投资吗?”),导致下游API被限流。加入频控后,自动降级为“根据历史数据建议”,问题解决。
工具编排的终极形态不是“LLM决定调哪个”,而是LLM只决定“调什么”,路由层决定“调哪个实例”。例如“查余额”工具,路由层根据account_id前缀自动分发到不同银行的API网关,LLM完全无感。
3.3 决策点三:记忆刷新——不是定时清理,是按价值衰减
记忆不是“满了就删最老的”。我们按信息价值衰减模型刷新:
- 用户指令类记忆:时效性高,2小时后衰减系数0.8,24小时后归零;
- 用户偏好类记忆:稳定性高,每周衰减系数0.95,半年后仍保留;
- 业务规则类记忆:静态不变,永不衰减,只随版本更新覆盖。
用Redis Sorted Set实现,score为current_timestamp - decay_factor * age_seconds。每次读取时,自动剔除score低于阈值的记忆。
在教育Agent中,学生说“我不喜欢视频讲解”,这条记忆衰减快(2天后权重归零);而“数学薄弱”这条记忆衰减慢(30天后仍有效)。LLM提示词里动态注入不同权重的记忆,效果提升明显。
3.4 决策点四:循环终止——不是固定步数,是多维熔断
终止条件必须多维:
- 步数熔断:全局最大12步;
- Token熔断:单次LLM调用输入+输出 > 7K tokens,强制压缩context;
- 耗时熔断:单轮循环 > 15秒,终止并返回“处理中,请稍候”;
- 置信度熔断:LLM自评
confidence_score < 0.6,触发人工审核队列。
某次压测发现,当并发从100升到500时,TOOL_FAILURE_LOOP触发率飙升——不是工具问题,而是LLM在高负载下生成参数错误率上升。于是增加负载感知熔断:当CPU > 85%持续10秒,自动将MAX_STEPS_EXCEEDED从12降至6,优先保障成功率。
3.5 决策点五:错误处理——不是try-except,是分级响应协议
错误不是异常,是协议的一部分。我们定义三级响应:
| 等级 | 示例 | 响应策略 |
|---|---|---|
| L1(可恢复) | ConnectionTimeout,RateLimitExceeded | 自动重试(指数退避),更新工具状态 |
| L2(需干预) | InvalidParameter,DataNotFound | 返回结构化错误,引导用户修正输入 |
| L3(不可恢复) | AuthenticationFailed,ServiceUnavailable | 切换备用工具链,或返回兜底文案 |
关键创新是错误指纹化:对每个错误码生成唯一指纹(如ERR_AUTH_0x3a7b),同一指纹24小时内出现>5次,自动触发告警并冻结该工具调用。
3.6 决策点六:并发扛压——不是加机器,是请求整形
“AI Agent怎么扛并发”本质是请求整形问题。我们不用简单加节点,而是:
- 前端队列:Nginx层按
user_id哈希分流,保证同一用户请求顺序执行; - 后端批处理:将100ms窗口内的相似请求(如同一
account_id的余额查询)合并为单次调用; - 结果缓存:对
get_account_balance类工具,结果缓存30秒,命中率从42%升至89%。
在期货交易Agent中,行情查询QPS从3000压到500,但用户感知延迟反降30%——因为批处理消除了大量重复请求。
3.7 决策点七:安全加固——不是加防火墙,是数据流染色
Agent安全不是“防黑客”,是防数据污染。我们实施:
- 输入染色:用户输入打上
source=web/source=app标签,不同来源走不同清洗规则; - 工具隔离:金融类工具运行在独立容器,网络策略禁止访问非必要端口;
- 输出净化:所有LLM输出经正则过滤(如屏蔽
SELECT * FROM users类SQL片段),再送下游。
最有效的措施是记忆隔离:用户A的对话记忆绝不进入用户B的context,哪怕他们用同一台设备。用session_id作为Redis Key前缀,物理隔离。
4. 实操:从零搭建一个抗压Agent(以银行客服为例)
现在我们动手,用真实代码片段,演示如何落实上述决策点。目标:一个能处理“查余额、转账、挂失”的银行客服Agent,要求支持500并发,错误率<0.5%。
4.1 环境与依赖
# Python 3.10+ pip install fastapi uvicorn pydantic redis qwen2 transformers torch # 向量库用Chroma(轻量,适合演示) pip install chromadb核心依赖选择理由:
- FastAPI:异步支持好,内置OpenAPI,便于监控埋点;
- Qwen2-7B-Chat:实测在金融文本上稳定性最佳;
- Chroma:单机部署,内存占用低,适合初期验证;
- Redis:作为记忆中枢,支持Pub/Sub做实时状态同步。
注意:不要用LangChain等大框架。它们抽象层太厚,debug时你根本不知道哪一行在阻塞。我们用原生库,控制力更强。
4.2 七要素落地代码骨架
# agent_core.py from typing import Dict, List, Optional, Any import redis import chromadb from pydantic import BaseModel, Field from transformers import AutoTokenizer, AutoModelForCausalLM import torch class Goal(BaseModel): user_id: str = Field(..., pattern=r"^U\d{8}$") intent: str = Field(..., enum=["balance", "transfer", "loss_report"]) amount: Optional[float] = None target_account: Optional[str] = None class MemoryManager: def __init__(self, redis_client: redis.Redis): self.redis = redis_client def get_context(self, session_id: str) -> str: # 按衰减模型组装context short_term = self._get_short_term(session_id) long_term = self._get_long_term(session_id) return f"【短期】{short_term}\n【长期】{long_term}" class ToolRegistry: def __init__(self): self.tools = {} def register(self, name: str, func, schema: Dict): # 注册时做schema校验 self.tools[name] = {"func": func, "schema": schema} def call(self, name: str, params: Dict) -> Dict: # 熔断、超时、降级全在这里 try: return self.tools[name]["func"](**params) except TimeoutError: return {"status": "error", "fallback": "cached_result"} class AgentLoop: def __init__(self, llm, tokenizer, memory, tools): self.llm = llm self.tokenizer = tokenizer self.memory = memory self.tools = tools self.max_steps = 12 def run(self, goal: Goal, session_id: str) -> Dict: step_count = 0 while step_count < self.max_steps: # 1. 构建prompt(注入记忆、工具描述、约束) prompt = self._build_prompt(goal, session_id) # 2. LLM推理(带token熔断) inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda") if inputs.input_ids.shape[1] > 7000: raise ValueError("Context overflow") outputs = self.llm.generate( **inputs, max_new_tokens=512, temperature=0.3, # 降低随机性 do_sample=False ) # 3. 解析LLM输出(强制JSON Schema校验) action = self._parse_action(outputs[0]) # 4. 执行工具 result = self.tools.call(action["name"], action["params"]) # 5. 判断终止条件 if result.get("final_answer"): return result if self._should_terminate(result, step_count): break step_count += 1 return {"status": "failed", "message": "Max steps exceeded"}4.3 关键决策点代码实现
决策点三:记忆刷新(按价值衰减)
# memory_manager.py import time import json from redis import Redis class MemoryManager: def __init__(self, redis_client: Redis): self.redis = redis_client def _get_short_term(self, session_id: str) -> str: # 短期记忆:2小时有效,用Sorted Set存 now = time.time() cutoff = now - 2 * 3600 # Redis ZREMRANGEBYSCORE 删除过期项 self.redis.zremrangebyscore(f"short:{session_id}", 0, cutoff) # 取最新3条 items = self.redis.zrevrange(f"short:{session_id}", 0, 2, withscores=True) return "\n".join([item[0].decode() for item in items]) def _get_long_term(self, session_id: str) -> str: # 长期记忆:用户偏好,用Hash存,永不自动删除 prefs = self.redis.hgetall(f"long:{session_id}") return json.dumps({k.decode(): v.decode() for k, v in prefs.items()})决策点六:并发扛压(请求整形)
# rate_limiter.py from fastapi import Depends, HTTPException from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.post("/chat") @limiter.limit("500/minute") # 全局限流 async def chat_endpoint( request: ChatRequest, background_tasks: BackgroundTasks, redis_client: Redis = Depends(get_redis) ): # 用户级队列:按user_id哈希到不同队列 queue_key = f"queue:user_{hash(request.user_id) % 4}" # 将请求推入队列 redis_client.rpush(queue_key, json.dumps(request.dict())) # 启动后台任务消费队列 background_tasks.add_task(process_queue, queue_key) return {"status": "queued", "queue_position": redis_client.llen(queue_key)}决策点七:安全加固(输入染色与输出净化)
# security_guard.py import re class SecurityGuard: def sanitize_input(self, text: str, source: str) -> str: # 按来源应用不同规则 if source == "web": # Web端:过滤script标签、onerror等 text = re.sub(r"<script.*?>.*?</script>", "", text, flags=re.DOTALL) elif source == "app": # App端:只允许中文、数字、常见符号 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s\.\,\!\?\;]", "", text) return text.strip() def sanitize_output(self, text: str) -> str: # 净化LLM输出:移除潜在危险模式 dangerous_patterns = [ r"SELECT\s+\*\s+FROM", r"DROP\s+TABLE", r"UNION\s+SELECT", r"sleep\(\d+\)", ] for pattern in dangerous_patterns: text = re.sub(pattern, "[REDACTED]", text, flags=re.IGNORECASE) return text # 在Agent调用前 guard = SecurityGuard() clean_input = guard.sanitize_input(user_input, source="web") # 在LLM输出后 safe_output = guard.sanitize_output(llm_output)4.4 压测与调优实录
我们用Locust对Agent进行压测:
# locustfile.py from locust import HttpUser, task, between class AgentUser(HttpUser): wait_time = between(1, 3) @task def chat(self): self.client.post("/chat", json={ "user_id": f"U{random.randint(10000000, 99999999)}", "message": random.choice([ "查一下我的余额", "转账1000元到账户ACC12345678", "挂失银行卡" ]) })压测结果(500并发):
| 指标 | 初始值 | 优化后 | 提升 |
|---|---|---|---|
| P95延迟 | 4.2s | 1.3s | 69% |
| 错误率 | 8.7% | 0.4% | 95% |
| CPU峰值 | 98% | 62% | — |
| 内存占用 | 12GB | 4.1GB | 66% |
关键优化动作:
- Context压缩:当输入长度>2K时,用TextRank算法自动摘要,保留核心实体和动词;
- 工具批处理:将同一用户的连续3次查询合并为单次DB查询;
- LLM推理优化:启用FlashAttention-2,GPU显存占用降40%;
- Redis连接池:从默认10连接升到200,连接等待时间归零。
实操心得:压测时别只看TPS。重点盯三个指标:
- LLM调用失败率:超过2%说明prompt或schema有问题;
- 工具调用超时率:超过5%说明下游服务或网络有问题;
- 内存泄漏速率:每小时增长>100MB,说明memory manager没清干净。
5. 常见问题与排查技巧实录
以下是我在7个项目中,高频遇到的12个问题,附真实日志、根因分析和一招解决法。全是血泪教训,没一句虚的。
5.1 问题1:LLM总在工具调用后“忘记”自己要干什么
现象:用户问“转账1000元”,LLM调用transfer_money工具后,下一步却开始聊天气。
日志片段:
[Step 1] LLM output: {"action": "transfer_money", "params": {"amount": 1000}} [Step 2] Tool result: {"status": "success", "tx_id": "TX123456"} [Step 3] LLM output: "今天天气不错,适合出门散步"根因:LLM的prompt里没强制要求“基于工具结果生成下一步”。它把工具返回当普通文本,而非决策依据。
解决:在prompt末尾加硬约束:
请严格遵循以下规则: 1. 如果工具返回{"status": "success"},必须基于返回内容生成下一步动作或最终回答; 2. 如果工具返回{"status": "error"},必须根据error_code生成用户可理解的提示; 3. 绝不允许生成与工具结果无关的内容。实测后,此类问题归零。
5.2 问题2:并发一上来,Redis内存暴涨后OOM
现象:并发从100升到300,Redis内存从2GB飙到16GB,然后OOM kill。
根因:短期记忆用LPUSH无限制堆积,没配maxmemory-policy。
解决:
- Redis配置加两行:
maxmemory 8gb maxmemory-policy allkeys-lru- 代码层加双重保险:
# 每次写入前检查 if redis_client.info()['used_memory_human'] > '6gb': # 清理最旧的10%短期记忆 redis_client.eval("redis.call('ZREMRANGEBYRANK', KEYS[1], 0, math.floor(redis.call('ZCARD', KEYS[1]) * 0.1))", 1, f"short:{session_id}")5.3 问题3:向量库检索越来越慢,最后超时
现象:Chroma里存了50万条故障案例,检索耗时从200ms升到8s。
根因:默认HNSW索引没调参,ef_construction太小,导致召回精度低,LLM要筛更多候选。
解决:重建索引时调参:
client.create_collection( name="fault_cases", metadata={"hnsw:construction_ef": 128, "hnsw:search_ef": 64} )construction_ef越大,建索引越慢但精度越高;search_ef越大,检索越准但越慢。我们取平衡值,耗时降到1.2s。
5.4 问题4:LLM生成的JSON总缺逗号,解析失败
现象:json.loads()频繁抛JSONDecodeError,日志里全是Expecting property name enclosed in double quotes。
根因:LLM的temperature太高(0.7),生成JSON时不稳定。
解决:
- 推理时
temperature=0.3,do_sample=False; - 输出后加JSON修复:
import json import re def fix_json(json_str: str) -> dict: # 补逗号 json_str = re.sub(r'([^,\{\[])\s*}', r'\1,}', json_str) # 补引号 json_str = re.sub(r'(\w+):', r'"\1":', json_str) return json.loads(json_str)5.5 问题5:用户说“算了”,Agent还在继续执行
现象:用户发“算了”,Agent却继续调用3个工具才停。
根因:循环机制没监听用户中断信号。
解决:在每步循环前检查:
def should_interrupt(self, session_id: str) -> bool: # 查Redis是否有新消息标记为interrupt return bool(self.redis.get(f"interrupt:{session_id}")) # 前端发“算了”时 redis.setex(f"interrupt:{session_id}", 300, "1") # 5分钟有效5.6 问题6:工具调用失败后,LLM反复重试同一错误
现象:get_account_balance因网络超时失败,LLM连续5次重试,拖垮整个会话。
根因:没实现TOOL_FAILURE_LOOP熔断。
解决:在ToolRegistry里加计数器:
class ToolRegistry: def __init__(self): self.failure_count = defaultdict(int) def call(self, name: str, params: Dict) -> Dict: try: result = self.tools[name]["func"](**params) self.failure_count[name] = 0 # 成功则清零 return result except Exception as e: self.failure_count[name] += 1 if self.failure_count[name] >= 3: # 触发熔断,返回降级结果 return self.tools[name].get("fallback", {"status": "error"}) raise5.7 问题7:不同用户会话记忆混在一起
现象:用户A的余额查询结果,出现在用户B的对话里。
根因:Redis Key没加session_id前缀,用了全局Key。
解决:所有Redis操作强制带前缀: