AI 审核
AI 审核是分析报告之上的一层可选解释:静态分析已经算出”谁受影响、为什么、走哪条路径、有多确定”,
模型只负责把这些事实写成评审要点。默认关闭,关闭时报告里没有 ai 字段、分析流程与改动前一致、
不向任何外部服务发请求,分析页也不会多出任何元素。
为什么摘要只解释事实,逐条意见只看变更片段
把整个仓库丢给模型问”这次改动会影响谁”,得到的是看起来合理但无法核对的答案:它可能漏掉一条真实调用链, 也可能编出一个不存在的类。两种错误在报告里长得一模一样。
所以摘要只接收静态分析结论。可选的「逐条评审意见」是另一段独立调用,只接收本次 diff 的 变更片段:它评论片段里真实存在的代码,不替静态分析决定”影响了谁”,也看不到片段以外的文件。
所以这条链路的方向是固定的:
① 静态分析产出事实(变更、影响路径、每跳用法、风险、置信度、盲区)
↓ 只把结论摘要(开启逐条意见时还有变更片段)送出去
② 模型用评审语言复述事实、排出人工确认的优先级
↓ 单独存放
③ report.ai,界面单独一张卡片并标注「待验证」
三条边界由此确定:
| 模型能做的 | 模型不能做的 |
|---|---|
| 解释某条影响为什么值得先看 | 决定”影响了谁”(影响清单由引擎产出) |
| 把技术路径翻译成评审语言 | 看到源码全文(只收到结论摘要与变更片段) |
| 指出需要人工确认的点 | 新增事实中不存在的类、方法或调用关系 |
| 在给出的变更片段内指出具体问题 | 评论片段以外的代码,或把”无法验证”当成删除理由 |
最后一条写在提示词里:禁止编造事实里没有的类、方法或调用,不确定就写”需人工确认”。 但提示词不是保证,所以输出与证据分区存放——读者始终能分清哪部分是静态分析的结论、哪部分是模型的解释。
配置
两种方式,服务不同场景。
设置页(日常调整)
「设置 → AI 审核」卡片。改完点「保存」立即生效,不必重启。
| 项 | 默认 | 说明 |
|---|---|---|
| 启用 AI 审核 | 关 | 总开关 |
| 接口地址 | 空 | 需以 http:// 或 https:// 开头;尾部 / 会被去掉 |
| 模型 | 空 | 端点上的模型名 |
| API Key | 空 | 本地模型通常不需要 |
| 每条分析最多送审影响 | 12 | 1..50;条目越多提示词越长,费用与耗时也越高 |
| 请求超时(秒) | 60 | 5..300 |
| 生成逐条评审意见 | 开 | 独立的第二段模型调用;发送变更片段而非源码全文(见「隐私边界」) |
| 每条分析最多评论 | 12 | 1..50;超出上限按严重级截断,能定位到行的评论优先保留 |
| 用反思模型过滤误报 | 开 | 每次报告额外一次模型调用;只删除与代码事实矛盾的评论(见「误报过滤」) |
| 默认在每次分析时生成 AI 审核 | 开 | 决定分析弹窗里那个勾选的初始状态 |
配置落在 {data-dir}/ai-settings.json(默认 ./.code-association/ai-settings.json,卡片底部显示绝对路径)。
写入是原子的(.tmp + ATOMIC_MOVE),POSIX 平台上尽量设为仅本人可读。
文件坏掉不影响启动。 读不到、解析失败、没有权限,一律退回 application.yml 的配置并记一条日志。
设置文件是使用者的便利,不该成为服务的启动依赖。
Key 只回显掩码。 长度不超过 8 时显示 ••••,否则显示前 4 字符 + … + 后 4 字符。
输入框的值永远是空的,掩码只出现在占位符里(「已保存 sk-a…3f9c · 留空保持不变」)。
保存时留空表示”不修改”,只有「清除」才真正清空——清除直接发一次 clearApiKey,
不做成”等下次保存才生效”,否则表单会显示已清除而服务端仍留着旧 Key。
为什么这一条必须写死在协议里:如果表单把掩码当明文回传,Key 会在第一次保存后被写成
sk-a…3f9c这样的字符串,之后每次调用都 401,而界面上看不出任何异常——用户只会以为服务坏了。
application.yml(部署统一配置)
code-association:
ai:
enabled: true
base-url: https://api.deepseek.com/v1 # 任意 OpenAI 兼容端点
api-key: ${AI_API_KEY:} # 从环境变量读,不要写进仓库;缺省为空
model: deepseek-chat
max-impacts: 12 # 1..50
timeout-seconds: 60 # 单次请求,5..300
# total-budget-seconds: 180 # 整段 AI 的总预算;不写按 timeout-seconds × 3 推导(90..900)
comments: true # 逐条评审意见(发送变更片段)
max-comments: 12 # 1..50
reflection: true # 反思模型过滤事实错误
default-for-runs: true
两者的关系:基线 + 快照覆盖
application.yml 是基线;一旦在设置页保存过,ai-settings.json 里那份完整快照会覆盖它。
想回到文件配置,删掉 ai-settings.json 重启。
当前生效值来自哪里,GET /api/ai 的 configuredBy 会回答(file 或 application.yml)。
覆盖而非逐项合并,是因为两个来源的语义不同:yml 属于部署形态,一份配置对所有实例生效; 设置页属于这台机器上的即时调整。合并会让”界面上改了却没生效”变成常态, 而删文件即回退保证了部署配置永远可复原。
就绪的判定
只有一个口径:已启用 + 接口地址非空 + 模型非空。
API Key 不算在内——本地 Ollama 一类的端点本来就没有 key。未就绪时 GET /api/ai 的 reason
给出缺了哪一项的中文原因(如”已启用但未填写模型”)。GET /api/health 的 aiReady 与
aiEnabled 报告同一件事,后者为兼容既有集成保留。
支持的端点
任意 OpenAI 兼容的 /chat/completions。地址去掉尾部 /,缺 /chat/completions 时自动补上,
所以填到 /v1 就够。
| 服务 | 接口地址 | 模型 | 备注 |
|---|---|---|---|
| DeepSeek | https://api.deepseek.com/v1 |
deepseek-chat |
需要 API Key |
| OpenAI | https://api.openai.com/v1 |
gpt-4o-mini |
需要 API Key |
| Ollama 本地 | http://localhost:11434/v1 |
qwen2.5:7b |
Key 留空;模型名以 ollama list 为准 |
设置页的快捷填充只填地址与模型,不动 Key。
测试连接
POST /api/ai/test 发一个最小请求:system 你是连通性测试助手。,user 只回复 pong 四个字母。,
max_tokens 为 8。请求体可以只带表单草稿(baseUrl / model / apiKey),不必先保存。
始终返回 200,用 ok 表达结果:
{ "ok": true, "latencyMs": 1234, "model": "deepseek-chat",
"endpoint": "https://api.deepseek.com/v1/chat/completions",
"message": "连接正常,模型已返回响应", "sample": "pong" }
失败时 ok=false、message 是中文可读原因、endpoint 仍返回实际调用的地址、sample 为空串。
未配置地址或模型时直接返回 ok=false 并说明,不发请求。
为什么不是 4xx/5xx:这是一次探测结果,不是服务端处理失败。用状态码表达”连不上对方的模型服务”, 会让调用方和日志都把它当成自己的故障。
使用
三层可选项
| 层次 | 怎么控制 | 缺省行为 |
|---|---|---|
| 单次分析 | 分析弹窗里的「AI 审核」勾选 | 取「默认在每次分析时生成」 |
| 默认值 | 设置页的「默认在每次分析时生成」 | 已配置时默认勾选 |
| 历史报告 | 分析页 AI 卡片上的按钮 | 不自动补生成 |
POST /api/analyses 的 aiReview 字段缺失时按「默认值 && 就绪」决定;显式 false 表示这次不生成。
未就绪时勾了也不会让分析失败——跳过并记一条日志。AI 是可选项,不该有能力拖垮必选项。
AI 段从 96% 起,按阶段把百分比推到 99%,detail 行依次是:
| 阶段 | detail |
|---|---|
| 摘要 | AI 评审:生成评审要点 |
| 逐条意见开始 | AI 评审:逐条评审意见 |
| 每个评审分块 | AI 评审:分块 i/n |
| 每次评论重定位 | AI 评审:重定位意见 i/n |
| 反思 | AI 评审:反思意见 |
| 收尾 | AI 评审:整理意见 |
百分比由「那一段工作自己」的完成比例换算:96 + round(3 × fraction),夹在 96..99。AI 段最多有十几次串行调用,
过去整段只写一次 96%,与卡死无法区分。100% 仍然由 finishAnalysis 独占写入——报告落库之前不宣布完成。
「逐条评审意见」「分块」「重定位」「反思」这四行只在「生成逐条评审意见」打开时出现(反思还要求确有评论); 关闭时摘要之后直接进入「整理意见」。
分析页的四种状态
| 状态 | 界面 | 可用操作 |
|---|---|---|
| 已生成 | 待验证说明(report.ai.disclaimer)+ 正文 + 逐条意见(如有)+ 脚注(模型 / 生成时间 / 送审条数) |
重新生成 |
| 生成失败 | 红色提示 + 失败原因,并注明静态分析结论不受影响 | 重试 |
| 有报告但没生成,AI 就绪 | 一句说明:本次只包含静态分析事实 | 生成 AI 评审 |
| 有报告但没生成,AI 未就绪 | 整张卡片隐藏 | 无 |
没有报告时同样整张卡片隐藏,也不发任何请求。
最后一行是刻意的:一个从未启用的功能不该在每份报告上留一个空位。卡片只在「有内容要展示」或 「能立刻生成」时出现——这与「默认关闭时界面与改动前完全一致」是同一条约束。
补生成历史报告
curl -X POST http://127.0.0.1:9996/api/analyses/42/ai-review
{ "generated": true, "model": "deepseek-chat", "text": "……",
"disclaimer": "AI 解释仅基于下列静态分析事实生成,属于待验证的分析结论,不是已验证的事实。",
"generatedAt": "2026-10-27T21:40:00Z", "maxImpacts": 12,
"comments": [
{ "id": "c1", "severity": "MAJOR", "category": "CORRECTNESS", "title": "……",
"detail": "……", "suggestion": "……", "file": "src/main/java/…/OrderService.java",
"startLine": 42, "endLine": 44, "side": "NEW", "anchor": "HUNK",
"existingCode": "if (order == null) {", "evidence": ["C1", "I2"], "confidence": 0.82 }
],
"reviewStats": { "filesReviewed": 3, "filesSkipped": 1, "commentsGenerated": 4,
"commentsKept": 1, "commentsDroppedUnlocated": 1, "commentsDroppedFiltered": 2,
"commentsDroppedReflection": 0, "commentsMerged": 0, "relocationCalls": 1,
"reflectionRan": true, "estimatedTokens": 5120, "budgetSeconds": 180,
"budgetExpired": false, "notes": [] } }
| 情况 | 响应 |
|---|---|
| 未就绪 | 400 {"error":"AI 审核未启用或未配置完成:<原因>"} |
| 分析不存在 | 404 |
| 存在但没有可用报告(未完成 / 失败) | 409 {"error":"该分析还没有可用的报告"} |
| 模型调用失败 | 200 + {"generated": false, "error": "HTTP 401: ..."} |
成功后会写回 report_json 并同步内存里的报告缓存——不同步的话,刷新页面看到的还是旧内容。
为什么模型调用失败是 200:请求本身被正确处理了,只是解释没生成。用 5xx 会让调用方以为服务端坏了, 而实际情况是”上游模型拒绝了这次调用”,处理方式完全不同(改 Key,而不是重启服务)。
命令行
java -jar code-association.jar analyze --location . --peer /path/to/other-service \
--ai --out report.json --fail-on HIGH
JSON 报告写入 ai 字段,Markdown 报告多一个独立小节:
### AI 评审说明(待验证)
> AI 生成内容,仅基于下列静态分析事实,不是已验证的结论。
<正文>
<sub>模型 deepseek-chat · 生成于 …</sub>
有逐条意见时再多一节(没有评论时这一节不出现):
### AI 逐条评审意见(待验证)
> 行号由变更片段与文件内容匹配得出,不是模型写的;无法定位到具体行的评论已丢弃。
1. 🟠 **MAJOR** · 空指针判断顺序反了
- 位置:`src/main/java/…/OrderService.java:42-44`(新增行 · 定位 变更块内匹配 · 证据 C1 · 置信度 82%)
- 正文:……
- 建议:……
- 代码:`if (order == null) {`
统计:审阅文件 3 · 跳过文件 1 · 保留评论 1 · 未定位丢弃 1 · 过滤 2 · 重定位调用 1 · 反思已执行
未配置时打印一行提示,退出码不变。让一个可选项的不可用导致流水线变红, 等于把门禁的判定权交给外部服务。
时间预算
整段 AI(摘要 + 最多十几次评论调用)共享一个墙钟预算 code-association.ai.total-budget-seconds。
它只能写在 application.yml:这是部署形态的参数(什么端点、有多慢),不是每次运行的实验,
所以设置页与 {data-dir}/ai-settings.json 都不带它——快照若带上,第一次保存就会悄悄覆盖部署值。
code-association:
ai:
timeout-seconds: 60 # 单次请求,5..300
total-budget-seconds: 180 # 整段 AI;不写则按下面的规则推导
不写时按 timeout-seconds × 3 推导,并夹在 90..900 秒之间:默认 timeout-seconds: 60 得到 180 秒。
推导而不是必填,是为了让已经在设置页调大的单次超时同步抬高整段预算;上下限则保证慢的本地模型仍有
几次调用的余地,而死端点不会把一次分析占住一刻钟。
预算是硬上限。每次评论调用前检查剩余时间,单次请求的超时也收缩到剩余预算之内;预算耗尽后不再发起剩余调用, 已生成的部分照常保留。预算截断这一段有两种形态:
- pipeline 真的跳过了一次调用:
reviewStats.budgetExpired=true。 - 预算把最后一次调用的单次超时缩短了,而那次调用失败:没有任何调用被跳过(
budgetExpired=false), 但评论缺失的原因仍是预算。
出现任一形态时,reviewError 由「第一条具体错误 + ; + AI 评审超出时间预算(N 秒),已保留已生成的部分」合成;
第一条错误超过 200 字时先截断,保证预算那句在整行 300 字上限内永远保留。没有具体错误但被截断时只有预算句;
预算没有参与截断时原样保留第一条错误(如 HTTP 401: …)。
端到端实测的 reviewError:
第 1 个分块评审失败:模型调用超时(5 秒),请检查接口地址、网络或模型响应速度;AI 评审超出时间预算(8 秒),已保留已生成的部分
形态 (a)(真的跳过了调用)budgetExpired=true;形态 (b)(只有 2 个分块、预算被第 2 个分块吃完)
budgetExpired=false,但 reviewError 是同样的合成形态。
逐段明细在 reviewStats.notes:分块被截断记 AI 评审超出时间预算,剩余 N 个分块未评审,
重定位被截断记 AI 评审超出时间预算,剩余 N 条意见未重定位,反思被跳过记
AI 评审超出时间预算,跳过反思,保留全部评论。摘要本身失败时 report.ai.error 是
AI 评审超出时间预算(N 秒),已保留已生成的部分。
budgetExpired 只在真的跳过了模型调用时为 true;预算恰好用尽但所有调用都跑完是 false。
这个字段用于区分是否还有未送审的分块,不等于评论是否完整——形态 (b) 里评论同样不全,字段却是 false。
已生成的评论仍会走定位与过滤,只有没来得及重定位的会缺失;反思被跳过时不会删除任何评论。
单次调用撞上传输层看门狗时,错误里是
模型调用超时(N 秒,AI 评审时间预算已用尽),请检查接口地址、网络或模型响应速度
(预算未耗尽时没有「AI 评审时间预算已用尽」这一句)。
为什么需要这层预算,而不是只靠单次 timeout-seconds:
- 单次
HttpRequest.timeout只覆盖到响应头。服务端发出响应头后停止写 body 时,这个超时不会触发, 调用会一直阻塞(本机 JDK 21 实测)——分析就永远停在最后一个进度上,只能杀进程。 - 逐条评审让一次分析最多有 14 次串行调用(1 次摘要 + ≤6 个评审分块 + ≤6 次重定位 + 1 次反思)。 每次 60 秒上限加起来是一刻钟,这期间界面上只有一个不动的 96%,和「卡死」无法区分。
预算耗尽不是失败:静态分析结论早已算完并写入报告,AI 只是可选的最后一段。
逐条评审意见(行级定位)
除了整段说明,AI 审核还能给出逐条意见:每条带严重级、类别、标题、正文、建议与一段代码引用,
并锚定到 文件:行。硬约束只有一条——行号不由模型写。
为什么不让模型写行号
模型看到的是 diff 文本,它自己数行会错(+ 前缀、上下文行、被截断的 hunk 都会让它偏移),
而一条”看起来对但位置错了”的评论比没有评论更糟:评审者点过去发现代码对不上,就再也不会信任这一栏。
所以协议里没有行号字段。模型必须逐字引用 existing_code——它评论的那段代码;行号由本机确定性代码
拿这段引用去 diff 与文件里找。引用再准也找不到时,这条意见被丢弃(计入 commentsDroppedUnlocated),
而不是猜一个接近的位置。
三层定位
| 层 | 在哪找 | 何时使用 | anchor |
|---|---|---|---|
| ① hunk 文本匹配 | 本次 diff 的新侧;删除行回退旧侧 | 引用的是变更行或 hunk 上下文 | HUNK |
| ② 整文件扫描 | 本机读取的新版本文件 | 引用的是文件里其它未变更部分;需要本次分析提供的文件快照 | FILE |
| ③ 模型重定位 | 再问一次模型”这段代码在文件里哪里” | 前两层都没匹配上,最多 6 次调用 | RELOCATED |
| 失败 | — | 三层都失败,丢弃并计数 | NONE(不会进报告) |
匹配细节按”宁可丢、不可错”设计:
- 空白归一化、忽略空行:缩进被格式化改过、行尾空格不同都不影响匹配。
- 连续行匹配、新侧优先:引用多行时要求连续命中;同一段代码在新旧两侧都存在时取新侧。
- 歧义不猜:整文件里出现多处相同片段,或用引用查不到唯一位置时放弃,而不是取最近的一个。
- 历史报告降级:补生成历史报告时没有新版本文件(
FileContent.NONE),第 ② 层跳过, 只在 diff 里定位;统计会如实写出来。
重定位层是为”模型引用了文件里没变的那几行”准备的:这类评论如果直接丢弃,会把真实问题一起扔掉; 最多 6 次是成本上限——每次重定位都是一次完整模型调用。
误报过滤:确定性优先,模型只删事实错误
两层,顺序固定:
- 确定性过滤(不花钱、可复现):评论的文件必须是本次送审文件;引用的证据编号必须真实存在;
行号范围自洽(
startLine <= endLine);严重级 / 类别在白名单内;标题与正文非空;文件 + 行重叠 + 标题相似的重复项合并,保留更严重的一条;单文件与总数按上限截断。 被丢弃的评论在内部记录filterNote,但不写进报告的comments。 - 反思模型(可关):把保留下来的评论连同 diff 再问一次模型,只问一个问题——哪条评论与代码事实矛盾。 可删的理由只有三类:引用的代码不存在、片段反驳了论断、引用的符号不存在。 “价值低”“无法验证”“不够严重”都不是删除理由:把过滤器变成第二个评审者,只会把误报换成漏报。
反思调用失败或返回的 JSON 解析不出来时,保留全部评论。宁可在报告里多一条待验证意见, 也不能因为解析失败把正确的意见删掉。
token 预算
提示词是成本,也是上下文上限,所以每一步都有硬上限(常量在 AiReviewPipeline / TokenEstimator):
| 环节 | 上限 | 为什么 |
|---|---|---|
| 送审文件 | 20 个 | 一份报告能读的文件数与评审价值不成正比 |
| 单文件 diff | 约 4000 tokens | 单个超大文件不把整份预算吃掉;超预算跳过并计数 |
| 单 chunk 的 diff | 约 8000 tokens | 按 token 估算把 diff 切成多个 chunk,逐块调用;事实目录与规则另算 |
| 输出 | 每 chunk 最多 10 条评论 | 一次回答太长时模型更容易编造 |
| 重定位 | 6 次调用 | 每层重定位都是一次完整模型调用 |
| 反思输入 | 约 12000 tokens | 超预算时只复核前若干条(按严重级),其余保留而不是被静默丢弃 |
| 评论总数 | max-comments(默认 12,上限 50) |
前几条评论价值最高,长列表反而更难处理 |
跳过的文件、丢弃的评论都进 reviewStats,所以”评论很少”可以解释,而不是只能猜。
报告结构
report.ai.comments 每条:
| 字段 | 含义 |
|---|---|
id |
本次生成内的稳定编号,用于和反思结果对应 |
severity |
BLOCKER / CRITICAL / MAJOR / MINOR / INFO |
category |
CORRECTNESS / SECURITY / PERFORMANCE / RELIABILITY / MAINTAINABILITY / IMPACT |
title / detail / suggestion |
标题、正文、建议 |
file / startLine / endLine / side |
位置;side=OLD 表示删除行 |
anchor |
定位层:HUNK / FILE / RELOCATED |
existingCode |
模型逐字引用的代码,定位的输入 |
evidence |
依据的事实编号(C1 / I2 / B3 等) |
confidence |
由定位层决定:HUNK 0.95 / FILE 0.85 / RELOCATED 0.9(不会采用模型自评分) |
report.ai.reviewStats 是这一轮的计数:filesReviewed、filesSkipped、commentsGenerated、
commentsKept、commentsDroppedUnlocated、commentsDroppedFiltered、commentsDroppedReflection、
commentsMerged、relocationCalls、reflectionRan、estimatedTokens、budgetSeconds(本次预算的秒数,
0 表示没有上限)、budgetExpired(真的跳过一次调用时为 true,即还有分块未送审;它不表示评论是否完整——预算缩短最后一次调用的超时并导致其失败时,评论同样不全而字段是 false)、notes(失败与预算相关的明细)。
逐条评审整体失败时另有可选的 reviewError;摘要照常保留——评论是加法,不能把已有的说明弄丢。
界面与导出
- 分析页 AI 卡片在摘要下按严重级渲染评论:彩色左边界 + 严重级标签 + 标题 +
文件:行, 正文、建议、「证据」编号、定位方式与置信度各占一行;代码引用用等宽字体原样展示。 - 评论为空数组时显示「本次变更未发现需要评论的问题」;老报告没有
comments字段时不新增任何区块。 - 底部一行统计(审阅 / 跳过 / 保留 / 未定位丢弃 / 过滤 / 反思删除 / 合并 / 重定位 / tokens)。
- Markdown 报告多一节「AI 逐条评审意见(待验证)」,位置、正文、建议、代码逐条列出,末尾一行统计; 没有评论时这一节不出现,输出与旧版本完全一致。
隐私边界
一句话概括:只发送变更片段与分析结论,不发送源码全文,也不发送仓库信息。
发送的内容(都是分析结论或变更片段,不是完整源码):
| 内容 | 字段 | 上限 |
|---|---|---|
| 变更清单 | 变更类型、模块、符号全名与签名、一句话摘要 | 15 条 |
| 影响条目 | 风险等级与分数、目标模块与符号、影响类型、置信度、深度、原因、逐跳路径与每跳用法 | max-impacts(默认 12) |
| 盲区 | 文件:行、无法解析的表达式、原因 | 8 条 |
| 变更片段(仅「逐条评审意见」开启) | diff 中的变更行及其上下文、文件路径与行号 | 20 文件 / 单文件约 4000 tokens / 单 chunk 约 8000 tokens |
两段调用分开授权、分开计数:摘要只发结论;逐条意见必须让模型看到变更片段才能评论。 关掉「生成逐条评审意见」就回到”只发摘要”,已接受旧隐私承诺的安装不需要改变行为。
不发送:
| 不发送的东西 | 原因 |
|---|---|
| 源码全文(未变更部分)与仓库其它文件 | 提示词只需要结论与变更片段;发整个文件等于把仓库交给第三方 |
| 仓库地址与远端 URL | 与解释无关,却会泄露内部项目位置 |
| Git 凭据 | 与模型服务无关 |
| API Key(回显 / 日志 / 错误信息) | 只出现在 Authorization 请求头里 |
第 ② 层”整文件扫描”容易引起误会,单独说明:它由本机的定位代码读取新版本文件,目的是把模型引用的 某段代码换算成行号。文件内容不进入提示词,只有匹配到的位置留在报告里;补生成历史报告时这一层直接跳过。
上限存在的理由很具体:报告规模随影响条数增长,不设上限时提示词会跟着涨, 最终既超出模型上下文,也让一次分析的费用不可预测。
仍然需要留意的一点:类名、方法名与模块名本身就在发送内容里。它们不是源码, 但足以暴露系统结构。对代码结构敏感的场景应先确认合规再启用——这也是它默认关闭的原因。
排错
状态显示「配置不完整」
看状态行右侧那句话,它会指出缺什么(接口地址 / 模型)。GET /api/ai 的 reason 是同一句话。
测试连接失败
测试结果区会显示 endpoint 与 message。先确认 endpoint 是不是预期的地址——
拼错地址时最容易发生的不是报错,而是打到一个能通但不对的服务上。其余情况见
故障排查 的「AI 审核失败」一节。
改了设置没生效
GET /api/ai 的 configuredBy 说明当前生效值来自哪里:
| configuredBy | 含义 |
|---|---|
file |
存在 ai-settings.json 且加载成功,它覆盖了 application.yml |
application.yml |
没有设置文件,用的是 yml 的配置 |
如果显示 file 却和 yml 不一致,这是预期行为——快照覆盖基线。想回到 yml,删掉文件重启。
分析页没有逐条意见
先确认设置页的「生成逐条评审意见」是开着的,再看卡片底部那行统计:未定位丢弃、过滤、跳过 分别说明
评论去哪了。三种常见情况:
| 现象 | 含义 |
|---|---|
| 显示「本次变更未发现需要评论的问题」 | 流水线跑完了,但没有评论通过定位与过滤;不是功能坏了 |
| 看不到评论区 | 老报告的 ai 里没有 comments 字段(生成时该开关是关的);重新生成一次即可 |
| 只有一行「逐条评审部分失败」 | 某几次模型调用失败,摘要照常;看 notes 与日志 event=ai.review 的 error |
分析页看不到 AI 卡片
先看 GET /api/health 的 aiReady。为 false 且这份报告里也没有 AI 内容时,卡片整张隐藏,
这是预期行为——未启用的功能不在每份报告上留空位。
配置保存成功是立即生效的,不需要重启。保存后仍看不到,按顺序检查:
GET /api/ai的ready是否为true(为false时看reason);- 接口地址是否以
http://或https://开头(否则保存会被 400 拒绝,界面上有 toast); - 模型名是否为空。
报告里已有历史 AI 内容时卡片照常显示,只是不再提供生成按钮。
相关接口
| 方法 | 路径 | 作用 |
|---|---|---|
GET |
/api/ai |
当前配置(Key 为掩码)+ ready / reason / configuredBy / settingsFile |
PUT |
/api/ai |
保存配置,立即生效;校验失败 400 + 中文原因 |
POST |
/api/ai/test |
测试连接,始终 200,用 ok 表达结果 |
POST |
/api/analyses/{id}/ai-review |
补生成 / 重新生成,返回 AI 对象本身 |
PUT 的校验规则(失败一律 400):
| 情况 | 提示 |
|---|---|
| 已启用但地址为空 | 已启用 AI 审核,但未填写接口地址 |
| 已启用但模型为空 | 已启用 AI 审核,但未填写模型 |
maxImpacts 不在 1..50 |
每条分析最多影响条数需在 1..50 之间 |
maxComments 不在 1..50 |
每条分析最多评论条数需在 1..50 之间 |
timeoutSeconds 不在 5..300 |
请求超时需在 5..300 秒之间 |
| 地址协议不对 | 接口地址需以 http:// 或 https:// 开头 |
日志只记录 enabled / baseUrl / model / maxImpacts / comments / maxComments,
不打印 API Key;逐条评审另记一行 event=ai.review(评论数、审阅文件数、丢弃与反思计数、耗时、错误)。