☰
项目拆解(二):模型输出是‘野‘的——三层兜底+重分析,把AI变成可靠零件
2026/10/13 10:23:53 网站建设 项目流程

项目拆解(二):模型输出是"野"的——三层兜底+重分析,把AI变成可靠零件

研发效能智能助手 · 第 2 个项目第 2 篇 | 上篇拆架构,这篇拆容错

上一篇把 AI 代码归档系统的骨架拆完了:FastAPI 四层结构、裸调 Ollama、Prompt 设计、SQLite 归档。评论区很多人在问同一个问题:“你那个模型返回的 JSON 要是坏了怎么办?模型崩了怎么办?”

问得好。这才是 AI 工程和调 API 玩具的分水岭。

这篇就把系统里最"见功夫"的部分全拆开:错误兜底的三层设计 + 重分析机制。所有代码都是项目里真实跑着的,每个坑都是真机踩出来的。

一、先说透:大模型是系统里最不可靠的零件

做传统后端的人有个习惯:相信自己的代码。写json.loads就觉得一定会成功,调接口就觉得一定会返回。

做 AI 工程,第一课就是把这种自信戒掉。大模型有三大"野":

  1. 会挂:Ollama 没启动、显存不够、机器卡死、请求超时
  2. 会疯:让它输出 JSON,它给你json ...包起来,或者直接输出一句"好的,我来分析一下:"
  3. 会骗:字段该是数组给你字符串,该是数字给你"五十",该有风险给你空

任何一个"野"没兜住,用户看到的就是白屏、报错、或者更糟——假结果。

所以这个系统从设计第一天就定了规矩:大模型只负责"提内容",工程负责"保可靠"。围绕一次分析,三层兜底,层层设卡。

二、错误全景图:一次分析要过四道鬼门关

用户点一下"让 AI 分析并存档",背后是这样的链路:

前端点击 → POST /api/analyze → 拼Prompt → 调Ollama → 解析JSON → 校验字段 → 入库 → 渲染上屏

每一段都可能挂。我们把故障点全部列出来,逐个设计对策:

环节会挂成什么样兜底层级
调 Ollama连不上 / 超时 / HTTP 500第一层:调用层
JSON 解析多字了 / 用代码块包了 / 结构不对第二层:解析层
字段校验类型错 / 缺字段第二层:解析层
前端渲染空数组 / 带空格空串 / 空指针第三层:展示层

下面一层一层拆。

三、第一层兜底:调用层,把技术错误翻译成人话

直接看代码(图1是 analyzer.py 的异常处理):

try:resp=requests.post(f"{OLLAMA_BASE_URL}/api/generate",json=payload,timeout=REQUEST_TIMEOUT,# 120秒,7b首次加载要冷启动)exceptrequests.exceptions.ConnectionError:raiseOllamaError("连不上 Ollama,请先在命令行执行: ollama serve")exceptrequests.exceptions.Timeout:raiseOllamaError(f"Ollama 超过{REQUEST_TIMEOUT}秒没响应,模型可能在加载或机器卡顿")ifresp.status_code!=200:raiseOllamaError(f"Ollama 返回 HTTP{resp.status_code}",raw=resp.text[:300])

三个设计细节,面试都能讲:

1. 超时给 120 秒,不是随便拍的。7b 模型在 4GB 显存上首次加载要冷启动,第一次请求慢得离谱。120 秒是实测出来的上限,后面改成了"真实倒计时"给用户看(见第五层),用户就不焦虑了。

2. 异常翻译成"人话"。ConnectionError对用户没有意义,但"连不上 Ollama,请先在命令行执行 ollama serve"是可执行的指令。AI 工程的产品化,就是让错误提示可行动。

3. 业务异常要带原始报文。OllamaError里存了raw=resp.text[:300]——前端只看到一句中文,但日志里能查到模型到底返回了什么。排查问题全靠这个尾巴。

这里有个反直觉的设计:底层异常一律翻译成业务异常抛出去,不在调用层 try 到底。翻译是职责,抛出是态度——路由层统一捕获,转成 502,前端统一渲染。错误处理最忌讳"每层都 try 一下,最后用户看到一脸懵"。

四、第二层兜底:解析层,JSON 坏了先别慌,降噪重试一次

第一层兜住了"调不通",但模型可能调通了却胡说八道。format=json只是让 Ollama 用 JSON schema 解码,不保证 7b 一定输出合法 JSON。实测它会干这些事:

  • 在 JSON 外面包 ```````json ````代码块
  • 输出"好的,我分析如下:"再跟 JSON
  • 字段名改了(language写成Language)
  • 直接整个 JSON 语法错误

解析层设计(图2是完整重试机制):

# 第一次:正常温度,先试raw=_call_ollama(prompt,temperature=0.1)analysis=_try_parse(raw)ifanalysisisnotNone:returnanalysis# 第二次:温度降到 0,再试一次logger.warning("第一次 JSON 解析失败,用 temperature=0.0 重试")raw=_call_ollama(prompt,temperature=0.0)analysis=_try_parse(raw)ifanalysisisnotNone:returnanalysis# 两次都失败,才认输raiseOllamaError("模型返回了无法解析的 JSON,可能是模型能力不足或代码太奇怪",raw=raw[:300])

核心逻辑是_try_parse——解析失败返回 None 而不是抛异常,让上层决定要不要重试:

def_try_parse(raw:str)->CodeAnalysis|None:raw=raw.strip()# 防御:有些模型还是会用 ```json ... ```包一下,剥掉ifraw.startswith("```"):raw=raw.strip("`")ifraw.lower().startswith("json"):raw=raw[4:].strip()try:data=json.loads(raw)returnCodeAnalysis(**data)except(json.JSONDecodeError,ValueError)ase:logger.warning("JSON 解析失败: %s; 原文前200字: %s",e,raw[:200])returnNone

三个关键点:

1. 先剥代码块再解析。7b 实测经常用 ```````json ````包输出——这是它训练语料的习惯。strip("")+ 去json` 字样,一次剥干净。这层防御让"输出格式不完美"从失败变成成功。

2. 重试时温度降到 0.0。第一次用 0.1 是想让模型"活一点"别太机械,但解析失败说明它已经疯了一次,重试就必须把随机性压到最低——temperature=0.0让输出退化成确定性,通常第二次就老实了。实测重试成功率挺高,用户几乎感觉不到这次重试。

3. 两次失败才认输,且带上原文。不无限重试(浪费算力、拖死用户体验),两次是实测出来的平衡点。认输时把raw[:300]塞进异常——开发者看到"模型返回了无法解析的 JSON"能立刻去日志翻原文,而不是瞎猜。

五、第三层兜底:展示层,模型输出的最后一道清洗

前两层都过了,数据入库了,最后一步是上屏。但这里还有坑——模型在"无风险"时会返回什么?

真机实测:它返回了[""],甚至[" "](带空格的空串)。前端直接渲染,用户看到一个红色的空警告框,吓一跳。

修复代码(index.html 里真实跑着的):

(a.risks||[]).map(t=>t&&t.trim()).filter(Boolean).length?a.risks.map(t=>`<span class="risk">${esc(t)}</span>`).join(""):'<span style="color:#34c759;">无,这段代码挺干净</span>'

注意细节:先t.trim()再filter(Boolean)。只filter(Boolean)挡不住[" "](带空格的字符串是真值),必须先清空格再过滤。这是真机踩出来的:第一次只写了filter(Boolean),第二天用户截图说"为什么没有风险还弹红框",一查是[" "]。

同一行的另一个细节:空的时候显示绿字"无,这段代码挺干净"。空数组不展示空容器,而是展示"无风险"的正面反馈——这种小细节决定了产品是"能用"还是"好用"。

六、展示层的另一个细节:真实倒计时,不骗用户

用户点了"分析",本地 7b 要跑 10-30 秒。进度条要不要?不要——因为我们自己都不知道还要多久,预估个假进度条,用户一眼看穿更糟。

方案是真实计时器(index.html):

conststartTs=Date.now();lettimer=setInterval(()=>{constsec=Math.round((Date.now()-startTs)/1000);$("result").innerHTML=`AI 分析中... 已用时${sec}秒(本地 7b 模型稍慢,请稍候)`;},1000);

每秒更新"已用时 X 秒"——数字是真实流逝的时间,不是拍脑袋的预估。用户体验:知道系统没死,知道大概要多久。同时分析期间按钮禁用($("btn").disabled = true),防止用户手滑重复提交把 Ollama 打爆。结果一到,finally里停计时器、恢复按钮。

小功能,但"等待"是所有 AI 产品最容易被吐槽的环节,值得较真。

七、重分析机制:一个"不覆盖"的设计取舍

系统里有已归档的片段。用户经常想:“这段代码当时分析漏了个风险,能不能重新分析?”这就是重分析需求。

最初的设计方案是"原地覆盖":重分析 → 更新同一条记录。但细想有两个问题:

  1. 历史被抹掉了。如果新分析反而更差(小模型不稳定),旧结果找不回来
  2. 无迹可循。看不到"上次怎么判的、这次怎么判的",没法对比模型进步

所以最终做了**“回填式重分析”**:点击列表里的片段 → 右侧展示结果 +左侧自动回填代码→ 用户改不改都行 → 再点"让 AI 分析并存档" →生成一条新记录。

交互上它就像"基于旧代码再分析一次",但数据上每次都留痕。旧记录还在列表里,新分析也入库了,两个结果摆在一起天然可对比。等以后模型换大版本,还能批量重跑一遍旧片段,看模型进步了多少——这是"覆盖式"永远给不了的价值。

顺手一提:点击回填这个动作还顺带实现了"查看详情"(点列表 → 右侧显示完整分析),一个交互两种用途,前端一行onclick="showDetail(id)"就搞定。

八、真机实测:这些错误场景,你迟早会遇到

最后交个底,这个系统在 4GB 显存 / 16GB 内存的机器上跑出来的真实错误场景:

场景现象兜底结果
Ollama 没启动就点分析ConnectionError中文提示"连不上 Ollama,请先执行 ollama serve"
模型首次加载冷启动请求挂起很久120 秒超时兜底,前端真实倒计时安抚
模型输出被 ```json 包裹json.loads 失败剥代码块后解析成功,用户无感知
模型输出非法 JSON第一次解析失败temperature=0.0 重试成功,用户无感知
连续两次都失败重试也失败抛"无法解析的 JSON"+原文前300字进日志
无风险时返回[" "]前端渲染空红框trim+filter(Boolean) 清洗,显示"无风险"绿字

你看,真正的 AI 工程能力,全在这些"用户无感知"的兜底里。用户只看到"结果出来了"或"一句人话报错",背后是三层设计在默默干活。

九、总结:兜底设计三条原则

  1. 每个不可靠点都必须有失败路径。从调用到解析到渲染,逐个列出故障点,逐个设计兜底,没有"反正不会坏"的侥幸
  2. 错误必须可读、可行动。技术异常翻译成人话,人话里带执行指令
  3. 兜底要可观测。重试要打日志、异常要带原文、结果要留痕——出了问题能在日志里还原全程

把这三条做到,你就把"调大模型的 Demo"升级成了"AI 工程"。


这套系统的完整源码(含开发文档、全部注释、可直接运行)在这里:

👉下载源码:AI代码片段智能归档系统-FastAPI+Ollama本地大模型-完整源码

📦资源地址:https://download.csdn.net/download/weixin_54679968/93681154

系列第三篇预告:Prompt 工程实战——怎么让 7b 小模型稳定输出 60%-70% 准确率(防照抄、防误报、边界认知全复盘)。关注不迷路。

关键词:FastAPI、Ollama、Qwen2.5、本地大模型、错误兜底、JSON容错、重试机制、Prompt 工程、Java+Python

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询