AI Agent 全栈开发问题排查记录
记录 AI Agent 应用全栈开发(架构 / RAG / 记忆 / 流式 / 限流 / 运营)中踩过的坑,每个问题都是实际遇到过并解决掉的。
问题一:首 token 延迟高,不知道慢在哪一跳
现象:用户反馈"点了发送要等好几秒才出字",日志里只有一个笼统的总耗时。
原因:从请求到第一个 token 要过:鉴权限流 → 上下文装载(记忆+历史折叠)→ RAG 检索(多源召回+rerank)→ 工具规划 → LLM prefill(随上下文长度线性涨)→ 网络。任何一段慢都算进 TTFT,不分段打点就是黑盒。
解决:每跳埋耗时打点,逐段归因;对慢环节做策略取舍——RAG 设超时兜底"宁可无检索也不拖死首 token",上下文装载失败降级为无记忆直答。
问题二:用了 Dify 又觉得被框架框住
现象:业务复杂后,Dify 流程图插不进自定义状态机逻辑,每一步 trace 也打不到自建监控。
原因:框架省的不是"写代码",而是状态持久化、可观测/回放、断点续跑、工具循环异常处理这些编排脏活;它收钱的地方是抽象泄漏与升级锁死。
解决:快速验证/低代码交付用 Dify/Coze;要私有化、精细控制、自建可观测、深度定制工具流时自研。自研的真实代价是四件套:状态图设计、失败恢复、trace、评估集,都要自己造。
问题三:上了多 Agent,效果反而更差
现象:拆成三个 Agent 协作后,成本翻倍、延迟叠加,三个子 Agent 的结论还互相矛盾。
原因:多 Agent 三个隐藏成本——① token × N;② 错误放大:A 的幻觉被 B 当事实继续推理,出错没人负责;③ 上下文碎片化:每个 Agent 只见局部,全局意图丢失,orchestrator 本身的 token 消耗与意图保持还是新瓶颈。
解决:默认单 Agent + 工具;只有任务可拆且子任务边界清晰(研究→写作→审查)、需要不同 persona/工具集时才上多 Agent,并显式设计"合并与冲突消解"的裁判环节。
问题四:RAG 召回 top-5 经常答非所问
现象:召回看着都有分数,回答就是不对;上线效果全凭感觉。
原因:没把"坏"归因到具体环节。召回侧和生成侧的指标混在一起,且"上下文污染"(塞入不相关内容)比不塞更糟。
解决:指标分侧建——召回侧 recall@k / hit rate,生成侧 faithfulness / answer relevance;检索结果做排序 + 截断 + 多源冲突消解;embedding 双塔粗排后接交叉式 rerank(top-100 → top-5 的漏斗),用少量延迟换精度。注意 faithfulness 100% 也可能是"上下文本身是错的"——评估要覆盖上下文质量。
问题五:BM25、向量、图谱的分数没法直接加权
现象:BM25 打 12 分、向量打 0.83、图谱打 0.5,加权和的权重怎么调都不对。
原因:三个异构源分数量纲、分布完全不同,"分数不可比"是融合的第一性矛盾,加权相加天然不鲁棒。
解决:用 RRF(Reciprocal Rank Fusion),只看排名不看分数,天然免疫各源分布差异:
function rrf(resultLists: string[][], k = 60): Map<string, number> {
const scores = new Map<string, number>()
for (const list of resultLists) {
list.forEach((docId, rank) => {
scores.set(docId, (scores.get(docId) ?? 0) + 1 / (k + rank + 1))
})
}
return scores
}
延伸:融合层要容错——某一路检索挂了返回空,不能把整体排序拉垮,各路召回贡献率要有监控;两个源返回相反结论时,把来源、时效、权威性一起交给模型,不默默拼接;答案带 provenance 可回指原文。
问题六:embedding 缓存层命中率惨不忍睹
现象:做了"embedding 缓存",线上命中率不到 3%,钱一点没省。
原因:线上用户 query 几乎不会原样重复,缓存向量本身命不中。真正能缓存的是"query → 最终答案"(省后续的检索 + 生成),不是 embedding。
解决:
- 分清两笔账:精确缓存按规范化文本 hash(大小写/空白/Unicode 归一化);语义缓存存问答对,相似度阈值宁可不命中也不给错答案
- 缓存 key 必须带"模型名 + 维度 + 规范化文本"复合键——embedding 模型一升级,不带版本 key 的旧向量全部语义失效
- 三层结构:进程内 LRU(快)/ Redis(分布式)/ 版本化失效(发版即清);命中率与节省成本要有线上数字
- 服务重启后命中率从 40% 掉到 5% 是冷启动冲垮热点,需要预热方案
问题七:召回命中的半截内容,把关键限定条件丢了
现象:知识里写着"仅限 VIP",但 chunk 边界正好把这半句切开,用户的问题只命中后半块,回答越权。
原因:固定长度切分会把语义单元拦腰斩断,跨块依赖信息丢失。
解决:parent-child 策略——用小块检索、回带父块或前后窗口进上下文;切分参数(size/overlap)靠评估集调,不拍脑袋;元数据(标题/层级/时间/权限)随块进向量库用于过滤溯源;Markdown/PDF/代码用结构感知切分。
问题八:分层记忆注入后,上下文预算还是爆了
现象:会话越长 token 越多,长期记忆注入又额外吃掉一大块预算,且 LLM 抽取的记忆会丢关键约束。
原因:只背了"短期/长期"概念,没设计每层的写入时机、容量、淘汰策略;LLM 自由文本摘要既会丢信息也会编造。
解决:
- 三层各管各的:工作记忆(本轮)、会话记忆(折叠成摘要,控制"摘要的摘要"膨胀)、长期记忆(用户画像/事实库)
- 长期记忆不每轮抽(贵),异步/空闲时批量做;画像事实用结构化槽位而非自然语言
- 每条记忆带时间戳、来源轮次、置信度,可被用户推翻;重要约束给更高保留优先级
- 冲突规则明确:用户最新指令 > 长期记忆(昨天"不吃香菜"、今天"破例",以今天为准),prompt 里物理顺序也要保证新指令在后
- 单独评估"摘要后关键信息保留率",不用对话流畅度糊弄
问题九:EventSource 断线重连后,消息不知道从哪继续
现象:SSE 断线自动重连了,但要么漏消息要么重复一段。
原因:原生 EventSource 只能 GET、不能带自定义 Header(鉴权得走 query/cookie),服务端没实现 id / Last-Event-ID 语义,重连后只能从头发。
解决:生产用 fetch + ReadableStream(要带 Authorization、要 POST、要 AbortController 真正取消);服务端每条事件带自增 id,重连时按 Last-Event-ID 从该 offset 续发,客户端按序去重。UTF-8 跨 chunk 解码的问题见流式对话前端踩坑记录。
问题十:点了"停止生成",token 还在计费
现象:用户点停止后前端确实不显示了,但看账单模型一直在生成到结束。
原因:前端 AbortController 只停了"接收";后端收到 abort 后没把取消信号转给上游 LLM,模型还在生成,只是没人收了。
解决:取消信号端到端传导——后端把 abort 传给模型 provider(SDK 支持 stream cancel);半截消息落库标记 status=stopped 并记录输出 offset,刷新后不能当完成态渲染;同一会话做生成互斥,防止停掉后 0.5 秒内的新消息和没停干净的旧流交错落库。
问题十一:LLM 生成到一半挂了,重试后出现重复 600 字
现象:上游 5xx 断连,直接重试,用户看到两段重复内容。
原因:LLM 生成不是幂等操作,重试会从头再来。
解决:兜底分级——
- 首包前失败(TTFT 超时/连接失败):可安全整轮重试或降级模型
- 中途失败:不盲重试。保留已生成部分提示用户续问,或把已生成文本当前缀让模型续写(接受接缝风险),或整轮丢弃改非流式一次生成后重推
- 超时分三层各报各的错:首包超时、流间静默超时、总时长上限
- per-provider/per-model 熔断:连续失败计数 + 半开探测 + 自动降级备胎
- 已显示的内容绝不回滚,错误以流尾事件平滑呈现
问题十二:IP 限流误伤了公司 NAT,防不住代理池
现象:上 IP 限流后,客户公司整栋楼报障;而攻击者换代理池一秒一个 IP,根本没拦住。
原因:IP 是网络位置不是身份。一个 NAT 出口背后几百个正常用户,攻击者却能低成本换 IP。
解决:IP 限流只做辅助防线(防单机风暴/初级 CC),主防线是用户维度配额 + 设备指纹;XFF 只信自己反代注入的信任链,取最后可信一跳(取最左 = 人人可伪造);IP 阈值宁可调高也不误杀企业用户。攻击者批量注册账号刷额度的问题属于注册风控,要在限流体系之外补一层。
问题十三:QPS 限流防不住 2 万 token 的长请求
现象:请求数限流很宽松,TPM 配额却天天被打爆,20 个长上下文并发就烧光。
原因:LLM 厂商限流是 RPM + TPM 双维度,请求数限流保护不了 token 配额。
解决:请求发出前做 token 预算预估(系统提示 + 历史 + 记忆 + 检索片段 + 工具 schema + max_tokens 预留),按"token 预算队列"而非"请求数队列"排队;长请求占大权重,防止把配额吃光饿死短请求;429 分语义处理——RPM 超限退避重试、TPM 超限降级(剪历史/缩检索)、配额耗尽提示用户别无限重试。
问题十四:活动流量 10 倍,排队还是丢弃
现象:流量暴涨后队列越排越长,队尾用户全部超时;下游推理一恢复就被新请求填满,队列永清不完。
原因:只做了"堆积"没做"拒绝",削峰的本质是保护下游 + 保整体 SLA,不是无限缓冲。
解决:
- 并发闸门(限 in-flight 数)+ 队列缓冲:优先级(付费/免费)、超时剔除、水位告警
- 队列满快速失败 503 + 前端"稍后重试",排队中返回排队信息而不是空白等待
- 降级通道:流量超限切小模型 / 关 RAG / 关多 Agent 只留直答,保核心体验
- 要有"拒绝部分请求保整体 SLA"的取舍,牺牲谁按优先级定
问题十五:1 万人同时问同一问题,LLM 只许调一次
现象:热点事件同一问题被万人同时问,逐条生成直接把下游和账单打爆。
原因:没有请求合并,相同请求各生成各的。
解决:singleflight + 结果缓冲扇出——相同 query+上下文 hash 合并上游调用,每条 SSE 连接维护自己的发送 offset,服务端把已生成 buffer 从该 offset 回放再实时续推:
const inflight = new Map<string, string>() // key -> 已生成 buffer
function subscribe(key: string, onChunk: (s: string) => void) {
const buffer = inflight.get(key)
if (buffer !== undefined) {
onChunk(buffer) // 后到者先回放已生成部分,再实时续推
subscribers.get(key)?.add(onChunk)
}
}
延伸:合并键绝不能包含用户私有上下文,否则 A 的个性化内容会泄给 B;某用户取消不能杀掉共享上游;生成完的结果进缓存直接秒回。诚实说边界:只对"完全相同的请求"有效,RAG 场景每人上下文略不同就合并不了。
问题十六:流式没结束用户又发一条,上下文错乱
现象:用户在流式中连发消息,或手机电脑同时开同一会话,出现消息乱序、半截覆盖、模型引用错乱。
原因:会话内并发没做互斥,跨端并发没做版本控制,"流式推送"和"最终落库"不一致。
解决:
- 单端:会话级生成互斥/串行队列,新消息要么排队要么顶掉旧的;半截消息标记
stopped,不参与下一轮上下文 - 跨端:消息带 revision 做乐观并发控制,客户端提交带
base_revision,冲突让后写者重拉合并 - 以最终落库为准渲染,流式只是预览,避免"推给用户了但没存上"
问题十七:Agent 埋点记的是点击,排查问题时什么也看不到
现象:传统 Web 埋点只记"按钮被点了",线上答非所问时无法定位是检索、模型还是工具的问题。
原因:Agent 应用要知道的是意图与推理轨迹,不是孤立事件。
解决:trace/span 模型——一次请求一个 trace(request_id 贯穿网关→Agent→RAG→LLM→工具),每个 step/工具调用/LLM 调用是一个 span,记录耗时、token、成本、输入输出摘要。全量存太贵就分层采样:错误/慢请求/低置信全量,正常请求按比例采样。核心指标:工具调用成功率、检索命中率、每会话成本、TTFT、用户放弃率。注意"LLM 没抛错但答错了"是质量事故不是系统事故,要接人工抽检/反馈回路才闭环。
问题十八:改了一版 RAG 和 prompt,拿不出上线前的证据
现象:换 embedding 模型 + 调 chunk + 改记忆模板,"感觉回答更顺了",但没人能证明没改坏。
原因:没有评估基线,效果验证靠玄学。
解决:
- 上线前:golden set 回归跑分 + 新旧版本 A/B + LLM-as-judge(校准位置偏见/冗长偏见)+ 人工抽审;换 embedding 要重灌索引并做召回对比
- 上线后:按 prompt 版本/模型版本聚合延迟、成本、失败率、反馈率对比,灰度和回滚一键切
- 诚实评估:评估集 200 条且分布可能偏,就要说明"线上 60% 是开放性问题、评估集 60% 是知识库原文题"的 gap,别把评估分数当唯一质量门禁
