Appearance
踩坑合集:现象 → 根因 → 解法
1. 本节产出
一张按「现象」组织的排障表:看到什么现象 → 最可能的根因 → 怎么验证 → 怎么修。按 Ctrl+F 搜现象关键词。
2. 前置依赖
无。遇到问题时来查。
3. 为什么按「现象」组织
按技术模块组织(Spring AI 的坑、RAG 的坑、Agent 的坑)在排障时没用——你遇到的是现象,不知道它属于哪个模块。所以这里按现象组织,每条标注相关章节。
4. 核心原理
排障的通用顺序:
1. 确认现象(到底哪里不对)
2. 缩小范围(是模型的问题还是你的代码的问题)
3. 二分定位(去掉一半功能看是否还复现)
4. 验证假设(改一个变量,看结果是否变化)第 2 步最重要:用一个「假模型」替换真实模型(01-11),如果问题消失就是模型侧问题,否则是你的代码问题。这能省掉一大半的排查时间。
5. 踩坑清单
5.1 启动与配置类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| 依赖拉不下来 | starter 命名在 1.x→2.x 变更 | 核对 00-03 版本矩阵,2.x 是 spring-ai-starter-model-* |
Could not resolve placeholder 'API_KEY' | 环境变量未导出或被 IDE 覆盖 | echo $API_KEY 验证;检查 IDE 启动配置 |
| 配置改了不生效 | 属性名拼错,被 Spring 静默忽略 | 打开 logging.level.org.springframework.ai: DEBUG 看实际值 |
| 启动报维度不匹配 | Embedding 维度与向量库建表维度不一致 | 检查 dimensions 与 VECTOR(n) |
| H2 建表成功、PG 建表失败 | 方言差异(JSONB / 窗口函数) | 用 Testcontainers 测真实 PG(01-11) |
5.2 调用与响应类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| 401 | Key 无效或带空格 | .trim();用 curl 单独验证 Key |
| 404 | base-url 多/少 /v1 | 通义兼容端点是 /compatible-mode/v1 |
| 连接超时 | 代理拦截 / 网络策略 | 先用 curl 验证网络,再查应用配置 |
| 响应很慢但不是超时 | 读取超时设太长,请求在排队 | 查网关队列与线程池 |
| 工具不触发 | 模型不支持,或 description 含糊 | 先用最简工具(返回固定值)验证模型能力(01-07) |
| 工具返回后模型照着念堆栈 | 工具抛了异常 | 异常转成人话文本返回(01-07) |
| 结构化输出解析失败 | temperature 太高 / 返回带代码块 | 设 temperature=0;用 BeanOutputConverter 清洗(01-08) |
模板渲染后出现 {xxx} | 变量名改了调用方没改 | 渲染后校验不得含 {(01-09) |
5.3 流式类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| 打字机不生效,一次性返回 | 缺少 produces = TEXT_EVENT_STREAM_VALUE | 加 produces;用 curl -N 验证 |
| 本地有效、生产无效 | Nginx proxy_buffering 缓冲了 SSE | 配 proxy_buffering off |
| 关闭页面后又触发请求 | EventSource 自动重连 | beforeunload 里 es.close() |
| WebSocket 连上就断 | HandlerMapping 顺序被抢 | setOrder(1)(01-05) |
| WebSocket 中途断开 | 代理 proxy_read_timeout 60s | 调超时 + 25s 心跳 |
5.4 RAG 类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| 怎么调都搜不到 | 离线段问题(解析/切分/元数据) | 打印片段肉眼看(02-04) |
| 检索到但答不对 | 片段被切断,语义不完整 | 检查切分是否切在句子中间 |
| 每个片段都带「第 3 页」 | 页眉页脚没去 | 跨页高频行过滤(02-03) |
| 搜 PDF 得到空 | 扫描件,需 OCR | 解析后校验字符数,为空直接报错 |
| 精度不高(排不准) | 缺 Rerank | 加二阶段排序(02-10) |
| 精确编号搜不到 | 纯向量不擅字面匹配 | 加 BM25 混合检索(02-09) |
| 不该答的答了 | 只有 prompt 约束,没有检索层拒答 | 加检索层硬拒答(02-11) |
| 多轮后答非所问 | 上下文超限被静默截断 | 调估算 Token,超限先压缩(01-01) |
| 换租户答不出 | 检索过滤把结果过滤没了 | 检查 filterExpression 是否正确(02-12) |
| 文档更新后答案没变 | 缓存没失效 | 发布/回滚联动清缓存(02-15) |
| A 看到 B 的数据 | 过滤漏了 tenantId(严重) | 跑 TenantIsolationIT 断言测试(02-12) |
5.5 Agent 类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| Agent 一直跑不结束 | 无迭代上限或上限太大 | 设三限额(轮次+Token+超时)(03-10) |
| 一夜烧掉很多钱 | 死循环 | 加重复动作检测与无进展检测 |
| 反复调用同一工具 | 工具返回失败但模型不理解 | 返回可行动的失败文本(03-10) |
| 模型调用不存在的工具 | 编造工具名 | 执行层白名单校验(03-11) |
| 跨租户操作 | tenantId 是工具参数 | 改为上下文注入(03-11) |
| 服务重启后任务丢失 | 状态只放内存 | 每轮落检查点(03-05) |
| 审批后仍被判超时 | 等待审批计入了总超时 | 恢复时重算 deadline(03-16) |
| 工具被自动重试造成重复业务 | 写操作自动重试 | 写操作禁止自动重试 + 幂等键(03-08) |
| 上下文越来越大 | 没做压缩 | 60% 触发摘要压缩(03-05) |
| 模型乱选工具 | 挂载工具太多(>10) | 动态筛选到 5~8 个(03-03) |
5.6 成本与性能类
| 现象 | 根因 | 验证 / 解法 |
|---|---|---|
| 账单对不上 | 用了估算值计费 / 超时请求未统计 | 对账只用 usage;超时单独标注(02-18) |
| 重试导致成本翻倍 | 无重试预算 | 加重试预算,后期轮次不重试(03-08) |
| 灌库拖慢在线服务 | 未做资源隔离 | 独立线程池 + 背压(02-16) |
| 检索突然变慢 | 元数据过滤没索引 | JSONB 建 GIN 索引(02-06) |
| 看不到 P95 | 没开 percentiles-histogram | 配置 percentiles-histogram(02-19) |
| Prometheus 内存暴涨 | 指标标签基数过高 | 标签只放低基数维度(02-19) |
| 缓存命中但内容过期 | 文档更新未失效缓存 | 按 sourceId 失效(02-15) |
6. 跑起来
排障时按表检索。建议打印出来贴在工位。
| 检查项 | 通过标准 |
|---|---|
| 能按现象找到条目 | Ctrl+F 搜关键词能命中 |
| 有验证方法 | 每条都有「怎么确认是这个原因」 |
| 指向正文 | 每条都标注了详细章节 |
7. 生产避坑
- 先区分「模型的问题」和「你的代码的问题」。用假模型替换一次(01-11),这个动作能排除掉一半的可能性。很多人花几小时调 prompt,实际问题是自己的解析代码。
- 「静默失败」类问题最危险:配置拼错不报错、解析出空文本不报错、上下文超限不报错。这类问题的共同防御是「显式校验 + fail fast」——宁可报错,不要静默。