从零到上线:RAG 知识库助手保姆级教程

2026年10月12日星期一·9810 字·49 分钟·
-浏览量
AI实践RAG与检索增强AIRAG
AI 摘要- DeepSeek

本笔记包含:RAG 原理 → 保姆级安装部署(每步含预期输出)→ 完整源码 → 故障排查复盘

从零到上线:RAG 知识库助手保姆级教程 - 封面图

项目:Qwen3 企业知识库助手(FastAPI + 单页前端 + 本地向量库) 本笔记包含:RAG 原理 → 保姆级安装部署(每步含预期输出)→ 完整源码 → 故障排查复盘 所有报错案例均来自真实部署过程,可直接对照排错。


目录#

  • 一、项目是什么
  • 二、RAG 原理(人话版)
  • 三、项目结构
  • 四、保姆级教程 Part A:云端部署(零基础照做)
  • 五、保姆级教程 Part B:本地 Ollama 私有化模式(Windows)
  • 六、日常运维
  • 七、进阶:设备健康日报(数据分析 × 大模型)
  • 八、完整源码
  • 九、故障排查复盘
  • 十、可扩展方向
  • 附录 A:验收测试文档全文(CNC-8800 设备维护手册)
  • 附录 B:四题验收测试用例(回归验证标准)

一、项目是什么#

0951.png
0951.png

210922.png
210922.png

企业知识库问答系统:上传企业文档(txt/md/pdf)→ 自动切分、向量化入库 → 用户提问 → 检索相关片段 → 大模型带引用回答。支持 SSE 流式输出、多轮对话、访问令牌鉴权,一套代码可在”云端 API”与”本地 Ollama 私有化”之间一键切换。

二、RAG 原理(人话版)#

RAG = 检索增强生成 = 先查资料,再回答(开卷考试):

plaintext
用户提问 → 问题向量化 → 向量库检索 Top-6 相似片段
→ 片段拼入提示词 → 大模型照着资料回答 → 附带 [1][2] 引用
环节本项目实现
文档解析txt/md 直接读,PDF 用 pypdf
切分(Chunking)280 字符、60 重叠,按中文标点语义边界断开
向量化百炼 text-embedding-v3 或本地 BGE-M3
存储numpy 矩阵 + JSON(生产可换 Qdrant,接口不变)
检索余弦相似度 Top-6,相关度 < 0.45 丢弃(防幻觉)
生成片段注入 system prompt,要求标注引用、找不到就明说

三、项目结构#

plaintext
qwen3-demo/
├── backend/
│ ├── main.py # FastAPI 主服务(问答 / 上传 / 日报 / 调试接口)
│ ├── rag_store.py # 切分 + 向量库 + 检索
│ ├── report.py # 设备健康日报(数据分析 × 大模型)
│ ├── data/ # equipment_data.csv 传感器数据 + 设备手册.txt
│ └── requirements.txt
├── frontend/index.html # 聊天界面 + 知识库侧栏 + 健康日报按钮
├── deploy.sh # 服务器一键部署脚本
└── README.md

四、保姆级教程 Part A:云端部署#

A0. 准备清单(开始前备齐)#

事项去哪办花费
云服务器 2核2G阿里云/腾讯云「轻量应用服务器」¥50–100/年
阿里云账号 + 百炼开通bailian.console.aliyun.com免费
项目代码包 qwen3-demo.zip本项目仓库/构建产物—

为什么 2核2G 够:服务器不跑模型,只做转发,推理在百炼云端完成。

A1. 购买并初始化服务器#

  1. 选「轻量应用服务器」→ 规格 2核2G(预算够选 2核4G)
  2. 地域选离你近的(如上海/杭州);镜像选 Ubuntu Server 22.04 LTS 64 位
  3. 系统盘 50G SSD 足够;时长建议年付(活动价更划算)
  4. 下单后进控制台做两件事:
    • 防火墙/安全组放行 80 端口(TCP,来源 0.0.0.0/0)——忘了这步浏览器打不开页面
    • 重置 root 密码并记好
  5. 记下实例的公网 IP

A2. 创建百炼 API Key#

  1. 打开 bailian.console.aliyun.com → 首次进入按引导免费开通
  2. 「API-KEY 管理」→ 创建 API-KEY → 复制
  3. 核对格式:sk- 开头 + 32 位左右字母数字,中间没有空格、点号、等号 (真实踩坑:复制错了来源,Key 里混入点号长串,部署后调用必然失败)
  4. ⚠️ Key 等同密码:不进聊天记录、不进代码仓库、只填服务器环境变量

A3. 把代码上传到服务器(二选一)#

方式 A:网页终端上传(最简单,推荐新手)

  1. 控制台点「登录」打开网页终端,用户名是 ubuntu
  2. 把电脑上的 qwen3-demo.zip 通过终端右上角文件夹(上传)图标上传
  3. 文件会落在 /home/ubuntu/ 下

方式 B:scp 命令上传(在你自己电脑的终端执行)

bashTerminal window
scp -r qwen3-demo root@服务器IP:/opt/

A4. 在服务器上执行部署(逐条命令 + 预期结果)#

打开网页终端,逐条复制执行(每条之间等上一条跑完):

bashTerminal window
# ① 切换到 root(提示符从 ubuntu@ 变成 root@)
sudo -i
bashTerminal window
# ② 安装解压工具(通常已装)
apt install -y unzip
bashTerminal window
# ③ 解压到 /opt(注意:文件名中间没有任何空格)
unzip /home/ubuntu/qwen3-demo.zip -d /opt/

✅ 预期输出:一串 creating: /opt/qwen3-demo/... 和 inflating: ...

bashTerminal window
# ④ 验证解压结果
ls /opt/qwen3-demo/backend

✅ 预期:看到 main.py rag_store.py requirements.txt

bashTerminal window
# ⑤ 配置环境变量(三条分别执行,= 后面换成你的值)
export LLM_API_KEY=sk-你的百炼Key
export ACCESS_TOKEN=起一个8位以上的访问令牌

✅ 预期:没有任何输出(Linux 的惯例:没消息就是好消息) ⚠️ 手动敲 export LLM_API_KEY=,然后只粘贴 Key 本身——整行粘贴容易带进多余字符

bashTerminal window
# ⑥ 一键部署
cd /opt/qwen3-demo
LLM_API_KEY=$LLM_API_KEY ACCESS_TOKEN=$ACCESS_TOKEN bash deploy.sh

✅ 预期:依次输出 1/5→5/5,最后打印 部署完成!浏览器访问: http://你的IP/?token=你的令牌

脚本自动完成:装依赖 → 配 2G swap → systemd 常驻服务(崩溃自动拉起、开机自启)→ Nginx 托前端 + 反向代理

A5. 验收测试(4 步)#

  1. 电脑浏览器打开 http://服务器IP/?token=你的令牌 → 看到”小千”聊天界面
  2. 发”你好” → 流式打字机回答 = 后端 + 大模型 API 通
  3. 手机关 Wi-Fi 用流量再开一次 = 公网访问无死角
  4. 「📚 知识库」上传一份文档 → 提问文档内容 → 回答带 [1][2] 引用 = RAG 全链路通

A6. 常见报错速查表(全部真实踩过)#

你看到的报错原因解决
cannot find or open /root/qwen3-demo.zip①命令里文件名被空格拆开 ②上传的文件在 /home/ubuntu 不在 /root写对 ~/qwen3-demo.zip;先 ls /home/ubuntu/ 确认位置
~export: command not foundexport 前多了粘贴进来的 ~ 字符手动敲 export LLM_API_KEY=,只粘贴 Key
LLM_API_KEY==sk-...: command not found长命令整行粘贴被终端拆碎 + 多打了一个 =改用分步 export(见 A4-⑤)
bash: deploy: No such file or directorydeploy .sh 中间多了空格写成整体 bash deploy.sh
unzip: cannot find ... .zip.ZIPzip 没传上来或路径不对find / -name "*.zip" 2>/dev/null 找位置
浏览器打不开页面安全组没放行 80 端口控制台 → 防火墙/安全组 → 放行 TCP 80
回答编造文档没有的内容低相关片段混入上下文相似度阈值提到 0.45 + 强化提示词
文档有的内容答错/说没有切分出问题(碎块/稀释)用 /api/debug 接口看命中片段,检查块数是否异常
解压时卡在 replace ...? [y]es, [n]o... 询问/opt 下已有旧文件,unzip 逐个询问覆盖用 sudo unzip -o(-o = 直接覆盖不再询问);问号处按 Ctrl+C 退出
Permission denied(解压/删除 /opt 下文件)/opt 是系统目录,普通用户 ubuntu 无写权限命令前加 sudo
服务反复重启,前端显示”无法连接后端”后端启动即崩溃(如缺依赖)① systemctl status qwen-api 看状态 ② journalctl -u qwen-api -n 30 --no-pager 看报错
日志出现 ModuleNotFoundError: No module named 'pandas'日报模块依赖 pandas,旧版 requirements 漏了sudo pip3 install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple 后重启服务
点日报按钮返回 {"detail":"令牌错误"}v6 前端 bug:聊天接口认请求头令牌、日报接口认 URL ?token= 参数,前端只发了前者升级到 v7 前端包(日报请求已带 ?token=);部署后记得把新 index.html 拷到 /var/www/html 并 Ctrl+F5
改了前端但页面没变化Nginx 展示的是 /var/www/html/index.html 的拷贝,unzip 只更新了 /opt 里的原件sudo cp /opt/qwen3-demo/frontend/index.html /var/www/html/index.html 后 Ctrl+F5 强刷

A7. 保姆级经验:粘贴命令的纪律#

网页终端粘贴长命令容易夹带字符(~、空格、换行)。三条纪律:

  1. 长命令拆短:环境变量用分步 export,别一行贴完
  2. 回车前逐字符核对:和预期命令比对一遍再按
  3. 每步看预期输出:A4 里每条命令都写了”预期输出”,不符就停下排查

五、保姆级教程 Part B:本地 Ollama 私有化模式(Windows)#

目的:让模型真正跑在自己电脑上(数据不出本机),也是”本地私有化部署”的演示素材。

B1. 安装 Ollama#

  1. 打开 ollama.com/download → 下载 Windows 版 → 双击安装
  2. 装完任务栏出现羊驼图标 = 后台服务已启动(无需任何配置)

B2. 拉取模型#

PowerShell 执行:

powershellTerminal window
ollama pull qwen3:8b # 约 5GB,网慢可以睡前挂着

B3. 验证(三连)#

powershellTerminal window
ollama run qwen3:8b "介绍一下你自己"

→ 能流式回答即成功。

powershellTerminal window
nvidia-smi

→ 应看到 python 进程占用数 GB 显存 = GPU 在干活(录演示视频必带这个画面)。

断网测试:关掉 Wi-Fi 再对话 → 依然能答 = 真·本地私有化。

B4. 把本项目切换成本地模式(Windows CMD)#

cmdTerminal window
cd qwen3-demo\backend
pip install -r requirements.txt
set LLM_BASE_URL=http://localhost:11434/v1
set LLM_API_KEY=ollama
set QWEN_MODEL=qwen3:8b
set EMBED_MODEL=bge-m3
uvicorn main:app --port 8000

前置:ollama pull bge-m3(嵌入模型,约 1.2GB)。 打开 frontend/index.html(直接双击即可,前端会自动回退到 localhost:8000)→ 上传文档 → 带引用回答,全程不联网。

B5. 录”私有化部署”演示视频脚本(2 分钟)#

  1. ollama pull qwen3:8b 过程(模型下载到本地)
  2. nvidia-smi 显存占用画面
  3. 终端对话演示
  4. 断网后继续对话(点任务栏网络图标断 Wi-Fi)→ “数据不出本机”
  5. (进阶)切到浏览器展示本地模式 RAG 问答

B6. 硬件参考(GTX 1650Ti 4G / 16G 内存实测)#

模型表现
qwen3:4b(2.6G 显存)完全进显卡,20–30 tok/s,丝滑
qwen3:8b(5.2G 权重)显存+内存混合推理,6–10 tok/s,可用
qwen3:14b+大部分压 CPU,2–4 tok/s,不推荐演示

六、日常运维#

bashTerminal window
systemctl restart qwen-api # 改代码/改配置后重启
journalctl -u qwen-api -f # 实时日志
curl -s http://127.0.0.1:8000/api/kb # 知识库状态
  • 更新代码:重新上传 zip → unzip -o /home/ubuntu/qwen3-demo-vX.zip -d /opt/ → systemctl restart qwen-api

  • 换 API Key / 令牌:

    bashTerminal window
    sed -i 's|LLM_API_KEY=.*|LLM_API_KEY=sk-新Key|' /etc/systemd/system/qwen-api.service
    sed -i 's/ACCESS_TOKEN=.*/ACCESS_TOKEN=新令牌/' /etc/systemd/system/qwen-api.service
    systemctl daemon-reload && systemctl restart qwen-api
  • 改了切分逻辑后:必须「清空知识库」重新上传文档(旧向量是旧切分生成的,不重建无效)

  • 知识库防重复:同一文档重复上传会追加入库不去重——重传前先清空


七、进阶:设备健康日报(数据分析 × 大模型)#

把”传感器数据分析”和”大模型解读”串成完整业务闭环 —— Pandas 算指标 → 检索手册建议 → 大模型写给车间主任看的日报。 这是整个项目里最能体现「制造业 + AI」岗位匹配度的功能,面试必讲。

7.1 为什么做这个功能#

做纯 RAG 问答(上传手册→提问→回答)只能证明”会调 API”,而工厂场景的真实痛点是:

  • 数据在传感器/SCADA 里躺着,没人看得懂、没人有空看;
  • 老师傅的经验在纸质手册里,新员工查不到;
  • 日报靠人工抄表写 Excel,滞后且主观。

本功能把它们串成一条自动链路:数据自己会说话,模型负责”翻译”成人话。这正是 FDE(制造业方向)岗位的核心工作:懂产线 + 会用 AI 解决真实问题。

7.2 整体架构(五步链路)#

plaintext
传感器 CSV ──► ①清洗 ──► ②双规则异常检测 ──► ③汇总 24h 指标
│
设备手册.txt ──► ④关键词检索处置建议
│
▼
⑤大模型生成自然语言日报(qwen-turbo)

关键认知:大模型不连传感器。它待在链路最上层,吃的是已经变成文本/表格的数据;采集层(PLC/Modbus/网关/SCADA)属于工业物联网范畴,两者通过数据库或文件交接。

对应代码两份(同一套逻辑,两种载体):

载体文件用途
融合版backend/report.py挂在 FastAPI 上,前端点按钮即出日报(接口 /api/report)
独立脚本device-analytics/daily_report.py命令行跑,配合 cron 定时推送

7.3 五个步骤逐个拆解(含代码)#

① 数据清洗 —— 工业数据的”脏”是常态#

python
def _clean(df: pd.DataFrame) -> pd.DataFrame:
df = df.drop_duplicates() # 去重复采集
for col in ["temperature", "vibration", "current"]:
df.loc[df[col] == 0, col] = np.nan # 0 值是断线,不是真 0
df = df.sort_values(["machine_id", "timestamp"])
for col in ["temperature", "vibration", "current"]:
df[col] = df.groupby("machine_id")[col].transform( # 关键:transform
lambda s: s.interpolate(limit_direction="both").fillna(s.median())
)
return df

三个要点(面试追问高发区):

  1. 0 值当缺失处理:传感器断线时常上报 0,直接拿去算会把均值拉爆,必须先转 NaN;
  2. 按设备分组插值:CNC-01 的缺失绝不能拿 CNC-02 的数据补,所以 groupby("machine_id");
  3. 为什么用 transform 而不是 apply:Pandas 新版本里 groupby().apply() 会丢失分组列(machine_id 列消失,后续逻辑直接报 KeyError),transform 返回与原表同索引的 Series,赋值回列天然对齐 —— 真实踩过的坑。

② 双规则异常检测 —— 规则 + 统计,互相兜底#

python
WINDOW, SIGMA = 48, 3.0
THRESH = {"vib_high": 4.5, "temp_high": 75.0}
# 规则 1:固定阈值(老师傅经验)
df["rule_alert"] = (df["temperature"] > 75) | (df["vibration"] > 4.5)
# 规则 2:滚动 3σ(跟自己比,能抓"慢性劣化")
m = g[col].rolling(48, min_periods=24).mean()
s = g[col].rolling(48, min_periods=24).std()
z = (当前值 - m) / s
g["sigma_alert"] = (temperature_z.abs() > 3) | (vibration_z.abs() > 3)
df["alert"] = df["rule_alert"] | df["sigma_alert"] # 任一触发即算异常

为什么两条规则都要:

固定阈值滚动 3σ
原理超过绝对标准偏离自身近期水平超过 3 个标准差
擅长抓突发性超标(如温度冲到 80℃)慢性劣化(轴承磨损:每天涨一点点,永远不超阈值)
弱点抓不到缓变对正常波动的机器偶有误报

窗口大小的权衡(实测验证过):窗口太小(如 6 点),均值/方差被当前值主导,计算不准、误报多;窗口太大,劣化趋势被”摊薄”,报警延迟,等于无效数据。实测 48 点 + min_periods=24 半窗起步,效果最平衡。

③ 汇总 24 小时指标 —— 给模型”喂”结构化数据#

python
latest = df[df["timestamp"] >= df["timestamp"].max() - pd.Timedelta(hours=23)]
prev = df[(df["machine_id"] == mid) &
(df["timestamp"] < df["timestamp"].max() - pd.Timedelta(hours=24))]
item = {
"设备": mid,
"平均温度": round(g["temperature"].mean(), 1),
"最高振动": round(g["vibration"].max(), 2),
"振动环比": f"{(vib_now - vib_prev) / vib_prev * 100:+.1f}%", # 环比前一个 24h
"产量": int(g["output_count"].sum()),
"异常次数": int(g["alert"].sum()),
}
# 三档风险分级
if 异常次数 >= 8 or 最高振动 > 5.3: 风险 = "高风险"
elif 异常次数 >= 3 or 最高振动 > 4.0: 风险 = "关注"
else: 风险 = "正常"

④ 检索手册处置建议 —— 让日报”有出处”#

python
def lookup_manual(keyword: str) -> str:
text = open(MANUAL, encoding="utf-8").read()
for block in text.split("\n\n"):
if keyword in block:
return block.strip()[:300]
return ""
# 触发逻辑:温度异常查「主轴过热」段落,振动异常查「振动」段落
tip = lookup_manual("主轴过热") if m["平均温度"] > 60 else lookup_manual("振动")

设计意图:日报不能只有”振动高了”,还得告诉车间主任该干什么(点检哪一项、加什么油、多少压力)。这里是轻量关键词检索;知识库模块的向量 RAG(余弦检索 + 0.45 阈值)是它的升级版 —— 两个版本都做过,面试可以对比讲。

⑤ 大模型生成日报 —— 提示词工程 + 防幻觉#

python
prompt = (
"你是工厂设备管理员助手。根据以下设备监控数据,写一份给车间主任看的《设备健康日报》。\n"
"要求:① 先一句话说总体情况;② 高风险设备单独列出问题和建议动作;"
"③ 语言简洁、面向一线,不要堆砌术语;④ 不超过 300 字。\n\n"
f"监控数据:\n{body}\n"
+ ("\n参考建议:\n" + "\n".join(hints) if hints else "")
)

防幻觉三板斧(和 RAG 问答模块同一思路):

  1. 只给结构化数字,不让模型”回忆”数据 —— 数字全部由 Pandas 算好,模型只负责组织语言;
  2. 给手册原文当参考建议,建议动作有出处;
  3. 限定读者(车间主任)+ 字数上限,输出稳定可读。

容错降级设计(工程素养体现):

  • 未配置 LLM_API_KEY → 只输出结构化数据,不报错;
  • 模型调用失败(401/网络)→ 降级为结构化数据 —— 日报功能永不因 AI 故障而中断;
  • 数据文件缺失 → 返回明确 error 提示。

7.4 实测输出(本项目真实结果)#

设备平均温度最高振动振动环比异常次数风险
CNC-0155.3℃3.04-1.9%0🟢 正常
CNC-0265.0℃6.44+97.6%23🔴 高风险
CNC-0354.9℃5.09+3.2%1🟡 关注

验证点:generate_data.py 在 CNC-02 的数据里埋了一个”轴承渐进劣化”剧本(振动逐日抬升),系统成功捕获 —— 振动环比 +97.6%、异常 23 次,判为高风险。这说明 3σ 检测 + 环比指标的组合确实能抓慢性故障。

大模型据此生成的日报示例:

“今日 3 台设备中 CNC-02 存在高风险:振动环比上升 97.6%、平均温度升至 65℃,建议检查主轴轴承,参考手册 2.1 节处置流程;CNC-03 需关注,CNC-01 正常。”

7.5 怎么用(三种方式)#

方式 1:网页点按钮(融合版)

前端「📊 设备健康日报」按钮 → 调 GET /api/report?token=令牌 → 同页展示结构化指标 + AI 日报全文。

bashTerminal window
curl "http://服务器IP/api/report?token=你的令牌"
# 返回 {"summary": {...}, "report": "……日报全文……"}

方式 2:命令行手动跑(独立脚本)

bashTerminal window
cd device-analytics
python3 daily_report.py # 无 Key 也能跑(只出结构化数据)
# 云端百炼
export LLM_API_KEY=sk-xxx && python3 daily_report.py
# 本地 Ollama(数据不出本机,适合保密车间)
export LLM_BASE_URL=http://localhost:11434/v1 LLM_API_KEY=ollama QWEN_MODEL=qwen3:8b
python3 daily_report.py

方式 3:cron 每天自动生成 + 企业微信推送

bashTerminal window
crontab -e
# 每天 07:30 生成日报并推送到企业微信群机器人
30 7 * * * cd /opt/device-analytics && /usr/bin/python3 daily_report.py >> /var/log/daily_report.log 2>&1

推送思路:拿到日报文本后 requests.post(企微webhook, {"msgtype": "text", "text": {"content": 日报}}),车间主任每天早班在群里直接看到。

7.6 本功能踩坑记录(真实排查)#

#现象根因修复
1groupby 之后 machine_id 列消失,后续 KeyErrorPandas 新版 groupby().apply() 丢失分组列改用 groupby(...).transform(...),索引天然对齐
2matplotlib 中文全是方框系统无中文字体,且字体族名不是想当然的名字fontManager.addfont() 注册 NotoSansCJK-Regular.ttc,族名实际是 Noto Sans CJK JP
3窗口调小后误报连篇 / 调大后报警滞后滚动窗口过短统计量失真,过长摊薄趋势定 48 点 + min_periods=24,并加环比指标补偿灵敏度
4report.py 读不到 CSV(backend/data/data 嵌套路径)打包时数据目录嵌套了一层移动到 backend/data/,路径统一用 os.path.dirname(os.path.abspath(__file__)) 相对定位
5部署后服务反复重启requirements.txt 漏了 pandas,日报模块 import 即崩补装 pip3 install pandas,并把 pandas 写回 requirements.txt
6点日报按钮返回”令牌错误”前端把令牌放请求头,而 /api/report 只认 URL ?token= 参数v7 前端修复:日报请求改为 "/api/report?token=" + encodeURIComponent(TOKEN)

经验:鉴权方式要全接口统一。聊天接口用 Depends(require_token) 读请求头,日报/调试接口却手写读 URL 参数,两套写法并存就埋了这种 bug——重构时应统一为一个依赖项。

7.7 面试怎么讲(90 秒版本)#

“我做过一个设备健康日报功能,解决的是工厂里数据没人看、经验传不下去的问题。链路是:传感器 CSV 先做清洗——0 值当断线缺失处理、按设备分组插值;然后双规则异常检测——固定阈值抓突发超标,滚动 3σ 抓慢性劣化,比如轴承磨损每天涨一点,固定阈值永远报警不了,3σ 就能抓到;接着汇总最近 24 小时指标,算振动环比;再从设备手册检索处置建议;最后让大模型把结构化数据翻译成给车间主任看的 300 字日报。数字全部由 Pandas 计算,模型只负责组织语言,所以不会幻觉出错误数据。我还做了降级:模型挂了就直接输出结构化数据,日报不断。真实数据里埋了一个轴承劣化剧本,系统成功抓到 CNC-02 振动环比涨了 97.6%,判为高风险。”

高频追问预案:

  • 为什么不用大模型直接算数据? —— 大模型算数不可靠且贵,正确的架构是”代码算数字、模型说人话”,各干各擅长的事。
  • 3σ 误报怎么办? —— 加环比和异常次数做三档风险分级,单点毛刺不会直接判高风险;生产上还可以加”连续 N 点超限才报警”的持久性判断。
  • 数据量大了怎么办? —— 现在 numpy 单机处理几万行足够;量大后检测可以下沉到时序数据库(如 TDengine/InfluxDB)的连续查询,日报只取聚合结果。
  • 和 RAG 什么关系? —— 同一套”检索→生成”思想:RAG 检索文档片段,日报检索手册处置建议;日报是”结构化数据 + 检索增强”的组合应用。

八、完整源码#

8.1 backend/main.py#

python
"""
Qwen3 演示项目 - 后端(含 RAG 知识库)
两种模式(环境变量切换):云端 API / 本地 Ollama 私有化
"""
import io
import json
import os
import httpx
from fastapi import Depends, FastAPI, Header, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from rag_store import VectorStore, chunk_text
# ---------------- 配置 ----------------
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
LLM_API_KEY = os.getenv("LLM_API_KEY", os.getenv("DASHSCOPE_API_KEY", ""))
CHAT_MODEL = os.getenv("QWEN_MODEL", "qwen-turbo")
EMBED_MODEL = os.getenv("EMBED_MODEL", "text-embedding-v3")
CHAT_URL = f"{LLM_BASE_URL}/chat/completions"
EMBED_URL = f"{LLM_BASE_URL}/embeddings"
ALLOWED_EXT = {".txt", ".md", ".pdf"}
# 公网部署必配:设置后所有 /api/* 请求需携带 X-Access-Token 头
ACCESS_TOKEN = os.getenv("ACCESS_TOKEN", "")
def require_token(x_token: str | None = Header(default=None, alias="X-Access-Token")):
if ACCESS_TOKEN and x_token != ACCESS_TOKEN:
raise HTTPException(401, "缺少或错误的访问令牌(联系管理员获取)")
app = FastAPI(title="Qwen3 RAG Demo API")
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
kb = VectorStore()
kb.load()
# ---------------- 基础工具 ----------------
async def embed_texts(texts: list[str]) -> list[list[float]]:
"""批量向量化(百炼单次最多 10 条,分批调用)。"""
out: list[list[float]] = []
async with httpx.AsyncClient(timeout=60) as client:
for i in range(0, len(texts), 10):
r = await client.post(
EMBED_URL,
headers={"Authorization": f"Bearer {LLM_API_KEY}"},
json={"model": EMBED_MODEL, "input": texts[i : i + 10]},
)
if r.status_code != 200:
raise HTTPException(502, f"向量化失败: {r.text[:300]}")
items = r.json()["data"]
out += [item["embedding"] for item in sorted(items, key=lambda x: x["index"])]
return out
def extract_pdf(data: bytes) -> str:
try:
from pypdf import PdfReader
except ImportError:
raise HTTPException(500, "服务器未安装 pypdf(pip install pypdf)")
reader = PdfReader(io.BytesIO(data))
return "\n".join(page.extract_text() or "" for page in reader.pages)
def extract_text(filename: str, data: bytes) -> str:
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED_EXT:
raise HTTPException(400, f"仅支持 {'/'.join(ALLOWED_EXT)} 格式")
if ext == ".pdf":
return extract_pdf(data)
return data.decode("utf-8", errors="ignore")
# ---------------- 接口 ----------------
class ChatMessage(BaseModel):
role: str = Field(pattern="^(system|user|assistant)$")
content: str
class ChatRequest(BaseModel):
messages: list[ChatMessage]
max_tokens: int = 1024
@app.get("/api/health")
def health():
return {"status": "ok", "model": CHAT_MODEL, "kb_chunks": kb.count}
@app.post("/api/upload")
async def upload(file: UploadFile, _=Depends(require_token)):
if not LLM_API_KEY:
raise HTTPException(500, "服务端未配置 LLM_API_KEY")
data = await file.read()
if len(data) > 10 * 1024 * 1024:
raise HTTPException(400, "文件超过 10MB 限制")
text = extract_text(file.filename, data)
if len(text.strip()) < 10:
raise HTTPException(400, "未能提取到有效文本内容")
chunks = chunk_text(text)
vectors = await embed_texts(chunks)
n = kb.add(file.filename, chunks, vectors)
return {"doc": file.filename, "chunks": n, "kb_total_chunks": kb.count}
@app.get("/api/kb")
def kb_status():
docs = {}
for m in kb.meta:
d = docs.setdefault(m["doc_id"], {"name": m["name"], "chunks": 0})
d["chunks"] += 1
return {"total_chunks": kb.count, "docs": list(docs.values())}
@app.delete("/api/kb")
def kb_clear():
kb.clear()
return {"status": "cleared"}
@app.get("/api/debug")
async def debug_search(q: str, token: str = ""):
"""调试接口:透视某问题的检索命中片段(排查 RAG 问题的关键工具)"""
if ACCESS_TOKEN and token != ACCESS_TOKEN:
raise HTTPException(401, "令牌错误")
if kb.count == 0:
return {"q": q, "kb_chunks": 0, "hits": []}
q_vec = (await embed_texts([q]))[0]
hits = kb.search(q_vec, top_k=6)
return {
"q": q, "kb_chunks": kb.count,
"hits": [{"score": h["score"], "text": h["text"][:150]} for h in hits],
}
@app.post("/api/chat")
async def chat(req: ChatRequest, _=Depends(require_token)):
if not LLM_API_KEY:
raise HTTPException(500, "服务端未配置 LLM_API_KEY")
last_user = next((m.content for m in reversed(req.messages) if m.role == "user"), "")
if not last_user:
raise HTTPException(400, "缺少用户消息")
# ---- RAG 检索 ----
sources, context_block = [], ""
if kb.count > 0:
q_vec = (await embed_texts([last_user]))[0]
hits = kb.search(q_vec, top_k=6)
if hits:
refs = "\n\n".join(
f"[{i + 1}] (来源:{h['name']})\n{h['text']}"
for i, h in enumerate(hits)
)
context_block = (
f"以下是知识库中检索到的参考资料:\n\n{refs}\n\n"
"回答要求(必须严格遵守):\n"
"1. 只能依据参考资料回答具体问题;若参考资料中找不到答案,"
"必须第一句就说明「知识库中没有相关内容」,不得编造数字或事实;\n"
"2. 依据资料回答时,在对应内容后标注引用序号,如 [1];\n"
"3. 参考资料与问题无关时,忽略资料,不要引用。"
)
sources = [
{"idx": i + 1, "name": h["name"], "score": h["score"],
"snippet": h["text"][:120] + ("…" if len(h["text"]) > 120 else "")}
for i, h in enumerate(hits)
]
system_prompt = (
"你是企业智能助手「小千」,回答简洁、专业、友好。"
+ (("\n\n" + context_block) if context_block else "")
)
payload = {
"model": CHAT_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
*[m.model_dump() for m in req.messages],
],
"max_tokens": req.max_tokens,
"stream": True,
"enable_thinking": False,
}
headers = {"Authorization": f"Bearer {LLM_API_KEY}"}
async def event_stream():
async with httpx.AsyncClient(timeout=120) as client:
async with client.stream("POST", CHAT_URL, json=payload, headers=headers) as r:
if r.status_code != 200:
body = (await r.aread()).decode("utf-8", "ignore")
yield f"data: {json.dumps({'error': body})}\n\n"
return
async for line in r.aiter_lines():
if line.startswith("data:"):
chunk = line[5:].strip()
if chunk == "[DONE]":
break
try:
delta = json.loads(chunk)["choices"][0]["delta"].get("content", "")
except Exception:
continue
if delta:
yield f"data: {json.dumps({'content': delta}, ensure_ascii=False)}\n\n"
if sources:
yield f"data: {json.dumps({'sources': sources}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)

8.2 backend/rag_store.py#

python
"""
轻量级本地向量知识库
- 不依赖外部数据库:向量存 numpy 文件、元数据存 JSON
- 中文友好切分(语义边界 + 重叠);余弦相似度检索
- 生产环境可平滑替换为 Qdrant/Milvus,接口不变
"""
import json
import os
import re
import numpy as np
STORE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
INDEX_PATH = os.path.join(STORE_DIR, "kb_index.npz")
META_PATH = os.path.join(STORE_DIR, "kb_meta.json")
def chunk_text(text: str, size: int = 280, overlap: int = 60) -> list[str]:
"""按长度切分,并在标点/换行边界尽量断开,保留 overlap 重叠。"""
text = re.sub(r"\r\n?", "\n", text)
text = re.sub(r"\n{3,}", "\n\n", text).strip()
if len(text) <= size:
return [text] if text else []
chunks, start = [], 0
while start < len(text):
end = min(start + size, len(text))
if end < len(text):
# 在窗口后 1/3 内寻找语义边界
window = text[start + size // 2 : end]
for sep in ["\n\n", "。", "!", "?", ";", "\n", ","]:
pos = window.rfind(sep)
if pos != -1:
end = start + size // 2 + pos + len(sep)
break
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
if end >= len(text):
break # 已覆盖到结尾,防止 start 以 1 字符步进死循环产生大量碎块
start = max(end - overlap, start + 1) # 防死循环
return chunks
class VectorStore:
def __init__(self):
os.makedirs(STORE_DIR, exist_ok=True)
self.vectors = None # ndarray (n, dim)
self.meta = [] # [{"doc_id","name","chunk_id","text"}]
def load(self) -> bool:
if os.path.exists(INDEX_PATH) and os.path.exists(META_PATH):
data = np.load(INDEX_PATH)
self.vectors = data["vectors"]
with open(META_PATH, encoding="utf-8") as f:
self.meta = json.load(f)
return True
return False
def save(self):
if self.vectors is not None:
np.savez_compressed(INDEX_PATH, vectors=self.vectors)
with open(META_PATH, "w", encoding="utf-8") as f:
json.dump(self.meta, f, ensure_ascii=False)
@property
def count(self) -> int:
return 0 if self.vectors is None else len(self.vectors)
def add(self, doc_name: str, chunks: list[str], vectors: list[list[float]]) -> int:
new_vec = np.array(vectors, dtype=np.float32)
self.vectors = new_vec if self.vectors is None else np.vstack([self.vectors, new_vec])
base = max((m["doc_id"] for m in self.meta), default=0) + 1
for i, c in enumerate(chunks):
self.meta.append({"doc_id": base, "name": doc_name, "chunk_id": i, "text": c})
self.save()
return len(chunks)
def search(self, query_vec: list[float], top_k: int = 4) -> list[dict]:
if self.count == 0:
return []
q = np.array(query_vec, dtype=np.float32)
scores = self.vectors @ q / (
np.linalg.norm(self.vectors, axis=1) * np.linalg.norm(q) + 1e-9
)
top = np.argsort(scores)[::-1][:top_k]
hits = []
for i in top:
if scores[i] < 0.45: # 相似度过低的丢弃(防弱相关上下文诱发幻觉)
continue
m = dict(self.meta[i])
m["score"] = round(float(scores[i]), 4)
hits.append(m)
return hits
def clear(self):
self.vectors = None
self.meta = []
for p in (INDEX_PATH, META_PATH):
if os.path.exists(p):
os.remove(p)

8.3 deploy.sh#

bash
#!/bin/bash
# 用法:代码放 /opt/qwen3-demo 后,root 执行:
# LLM_API_KEY=sk-xxx ACCESS_TOKEN=你的令牌 bash deploy.sh
set -e
if [ -z "$LLM_API_KEY" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "用法: LLM_API_KEY=sk-xxx ACCESS_TOKEN=你的令牌 bash deploy.sh"; exit 1
fi
[ "$(id -u)" != "0" ] && { echo "请用 root 执行"; exit 1; }
[ ! -d /opt/qwen3-demo/backend ] && { echo "请先把代码上传到 /opt/qwen3-demo"; exit 1; }
echo "==> 1/5 安装基础软件"
apt update -y && apt install -y python3-pip nginx curl
echo "==> 2/5 配置 2G swap"
if [ ! -f /swapfile ]; then
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
grep -q swapfile /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
echo "==> 3/5 安装后端依赖"
cd /opt/qwen3-demo/backend
pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
echo "==> 4/5 配置 systemd 常驻服务"
cat > /etc/systemd/system/qwen-api.service <<EOF
[Unit]
Description=Qwen3 RAG Demo API
After=network.target
[Service]
User=root
WorkingDirectory=/opt/qwen3-demo/backend
Environment=LLM_API_KEY=${LLM_API_KEY}
Environment=QWEN_MODEL=${QWEN_MODEL:-qwen-turbo}
Environment=ACCESS_TOKEN=${ACCESS_TOKEN}
ExecStart=/usr/local/bin/uvicorn main:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable --now qwen-api
echo "==> 5/5 配置 Nginx(前端 + 反向代理)"
cat > /etc/nginx/sites-available/default <<'EOF'
server {
listen 80 default_server;
root /var/www/html;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # SSE 流式必须关缓冲
proxy_read_timeout 300s;
}
}
EOF
cp /opt/qwen3-demo/frontend/index.html /var/www/html/index.html
nginx -t && systemctl reload nginx
sleep 2
curl -s http://127.0.0.1:8000/api/health && echo ""
IP=$(curl -s --connect-timeout 3 ifconfig.me || echo 服务器IP)
echo "部署完成!浏览器访问: http://${IP}/?token=${ACCESS_TOKEN}"

8.4 frontend/index.html(核心逻辑摘录,完整文件见仓库)#

javascript
// ① 后端地址与令牌:默认同源(Nginx 反代),可用 URL 参数覆盖
const params = new URLSearchParams(location.search);
const API = params.get("api") || (location.protocol === "file:" ? "http://localhost:8000" : location.origin);
const TOKEN = params.get("token") || "";
const authHeaders = TOKEN ? { "X-Access-Token": TOKEN } : {};
// ② SSE 流式读取(打字机效果核心)
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = "", full = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) >= 0) {
const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 2);
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
const j = JSON.parse(data);
if (j.error) throw new Error(j.error);
if (j.sources) { addSources(j.sources); continue; } // 引用来源标签
full += j.content || "";
bot.textContent = full; // 逐字渲染
chatEl.scrollTop = chatEl.scrollHeight;
}
}
// ③ 多轮对话记忆:滑动窗口只保留最近 10 轮,控制上下文成本
body: JSON.stringify({ messages: history.slice(-10) })
// ④ 设备健康日报按钮(v7:令牌同时放 URL 参数和请求头,两种鉴权接口都兼容)
document.getElementById("reportBtn").onclick = async () => {
const el = addMsg("bot", "📊 正在分析设备数据并生成日报…");
const r = await fetch(API + "/api/report?token=" + encodeURIComponent(TOKEN), { headers: authHeaders });
const j = await r.json();
el.textContent = "📊 设备健康日报(最近 24 小时)\n\n" + (j.report || JSON.stringify(j));
};

8.5 backend/report.py(设备健康日报,完整源码)#

python
"""
设备健康日报模块(融合进 qwen3-demo)
链路:传感器 CSV → 清洗 → 双规则异常检测 → 汇总 24h 指标
→ 检索设备手册处置建议 → 大模型生成自然语言日报
"""
import json
import os
import numpy as np
import pandas as pd
# 数据文件默认放在 qwen3-demo/backend/data/ 下,可用环境变量覆盖
DATA_CSV = os.getenv("EQUIPMENT_CSV",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "equipment_data.csv"))
MANUAL = os.getenv("EQUIPMENT_MANUAL",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "设备手册.txt"))
WINDOW, SIGMA = 48, 3.0
THRESH = {"vib_high": 4.5, "temp_high": 75.0}
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
LLM_API_KEY = os.getenv("LLM_API_KEY", "")
MODEL = os.getenv("QWEN_MODEL", "qwen-turbo")
def _clean(df: pd.DataFrame) -> pd.DataFrame:
df = df.drop_duplicates()
for col in ["temperature", "vibration", "current"]:
df.loc[df[col] == 0, col] = np.nan # 0 值是断线,不是真 0
df = df.sort_values(["machine_id", "timestamp"])
for col in ["temperature", "vibration", "current"]:
df[col] = df.groupby("machine_id")[col].transform( # transform 不丢分组列
lambda s: s.interpolate(limit_direction="both").fillna(s.median())
)
return df
def _detect(df: pd.DataFrame) -> pd.DataFrame:
def rolling_sigma(g: pd.DataFrame) -> pd.DataFrame:
for col in ["temperature", "vibration"]:
m = g[col].rolling(WINDOW, min_periods=24).mean()
s = g[col].rolling(WINDOW, min_periods=24).std()
g[f"{col}_z"] = ((g[col] - m) / s.replace(0, np.nan)).round(2)
g["sigma_alert"] = (g["temperature_z"].abs() > SIGMA) | (g["vibration_z"].abs() > SIGMA)
return g
df = pd.concat([rolling_sigma(g) for _, g in df.groupby("machine_id")]).sort_index()
df["rule_alert"] = (df["temperature"] > THRESH["temp_high"]) | (df["vibration"] > THRESH["vib_high"])
df["alert"] = df["rule_alert"] | df["sigma_alert"]
return df
def build_summary() -> dict:
"""读取设备数据,完成清洗与检测,汇总最近 24 小时指标。"""
if not os.path.exists(DATA_CSV):
return {"error": f"未找到设备数据文件:{DATA_CSV}"}
df = _detect(_clean(pd.read_csv(DATA_CSV, parse_dates=["timestamp"])))
latest = df[df["timestamp"] >= df["timestamp"].max() - pd.Timedelta(hours=23)]
rows = []
for mid, g in latest.groupby("machine_id"):
prev = df[(df["machine_id"] == mid) &
(df["timestamp"] < df["timestamp"].max() - pd.Timedelta(hours=24))]
vib_now, vib_prev = g["vibration"].mean(), prev["vibration"].mean()
item = {
"设备": mid,
"平均温度": round(float(g["temperature"].mean()), 1),
"最高振动": round(float(g["vibration"].max()), 2),
"振动环比": f"{(vib_now - vib_prev) / vib_prev * 100:+.1f}%",
"产量": int(g["output_count"].sum()),
"异常次数": int(g["alert"].sum()),
}
if item["异常次数"] >= 8 or item["最高振动"] > THRESH["vib_high"] + 0.8:
item["风险"] = "高风险"
elif item["异常次数"] >= 3 or item["最高振动"] > THRESH["vib_high"] - 0.5:
item["风险"] = "关注"
else:
item["风险"] = "正常"
rows.append(item)
return {"周期": "最近 24 小时", "设备": rows}
def lookup_manual(keyword: str) -> str:
"""从设备手册中按关键词取处置建议(轻量检索,生产可换 RAG 向量检索)。"""
if not os.path.exists(MANUAL):
return ""
text = open(MANUAL, encoding="utf-8").read()
for block in text.split("\n\n"):
if keyword in block:
return block.strip()[:300]
return ""
async def generate_report_text(summary: dict) -> str:
"""调用大模型把结构化指标翻译成自然语言日报。"""
import httpx
body = json.dumps(summary, ensure_ascii=False, indent=2)
hints = []
for m in summary.get("设备", []):
if m["风险"] != "正常":
tip = lookup_manual("主轴过热") if m["平均温度"] > 60 else lookup_manual("振动")
if tip:
hints.append(f"【{m['设备']}】参考手册建议:{tip}")
prompt = (
"你是工厂设备管理员助手。根据以下设备监控数据,写一份给车间主任看的《设备健康日报》。\n"
"要求:① 先一句话说总体情况;② 高风险设备单独列出问题和建议动作;"
"③ 语言简洁、面向一线,不要堆砌术语;④ 不超过 300 字。\n\n"
f"监控数据:\n{body}\n"
+ ("\n参考建议:\n" + "\n".join(hints) if hints else "")
)
if not LLM_API_KEY:
return "(未配置 LLM_API_KEY,以下为结构化数据)\n\n" + body
try:
async with httpx.AsyncClient(timeout=120) as client:
r = await client.post(
f"{LLM_BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {LLM_API_KEY}"},
json={"model": MODEL, "messages": [{"role": "user", "content": prompt}],
"enable_thinking": False},
)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
except Exception as e:
return f"(模型调用失败:{e})以下是结构化数据:\n\n{body}"

九、故障排查复盘(最值钱的部分)#

上线验收用固定测试文档出 4 道题,2 对 2 错,逐步定位修复:

问题 A:文档里没有的内容被编造#

  • 现象:问”年假有几天”(文档无此内容),模型自信回答”5 天”还带引用
  • 定位:低相关片段(0.46 分)混入上下文,误导模型
  • 修复:相似度阈值 0.3 → 0.45 + 强化提示词”找不到必须第一句就明说”
  • 结果:✅ 正确回答”知识库中没有相关内容”

问题 B:文档里有的内容答错#

  • 现象:问”冷却液浓度低于多少补加”,文档写了”低于 4%“,模型答错甚至说”没有”
  • 排查链路:
    1. 怀疑召回不足 → Top-4 扩到 Top-6 → 未解决
    2. 新增 /api/debug 调试接口透视检索中间产物 → 正确片段其实在候选里(0.56 分)
    3. 怀疑模型能力 → qwen-turbo 换 qwen-plus → 仍未解决
    4. 发现 kb_chunks=67 异常(1141 字符应为 ~7 块)→ 逐轮追踪切分循环
    5. 根因:切分器在文档尾部以 1 字符步进退化,产生 60 个重复碎块挤占检索结果
  • 修复:循环终止条件修复 → 67 块变 7 块,四题全对

方法论#

固定测试集做回归验证,每改一处重测全部用例; 排查遵循”先看中间产物,再改参数”;RAG 的可靠性来自检索质量与数据管道,而不只是模型。


十、可扩展方向#

  • 混合检索(BM25 + 向量)与 Reranker 重排
  • 片段去重与最小长度过滤、同一文档重复上传检测
  • 会话持久化、多租户按部门隔离
  • Dockerfile + 单元测试 + CI;Qdrant 替换本地 numpy 存储

附录 A:验收测试文档全文(CNC-8800 设备维护手册)#

用途:A5 验收测试与第九章回归测试的标准测试文档。把下面全文复制保存为 测试文档-设备维护手册.txt,通过「📚 知识库」上传即可复现全部测试用例。这份文档同时是日报模块读取处置建议的来源(backend/data/设备手册.txt 内容与之相同)。

text
CNC-8800 数控加工中心 设备维护手册(测试版)
第一章 日常点检规范
1.1 每日点检项目
开机前必须完成以下五项检查:
(1)检查液压油位,油位应处于油窗刻度的 1/2 至 2/3 之间;
(2)检查冷却液浓度,正常范围为 5%-8%,低于 4% 需补加原液;
(3)检查气压表读数,主轴气动夹具压力应保持在 0.6-0.8 MPa;
(4)检查导轨润滑油泵工作状态,油位低于刻度 1/4 时立即加注 46 号导轨油;
(5)Listen 检查(听音检查):主轴空运转 3 分钟,无异响、无异常振动方可开始加工。
1.2 每周点检项目
每周一早班对排屑器进行清理,清除铁屑积存;检查电柜风扇滤网,积尘厚度超过 2mm 时用压缩空气(0.4MPa 以下)吹扫;检查各轴限位开关动作是否灵活。
第二章 常见故障处理
2.1 主轴过热报警(ALM-4021)
故障现象:主轴温度传感器读数超过 65℃,系统触发 ALM-4021 报警。
处理步骤:
(1)先停机 15 分钟,让主轴自然冷却;
(2)检查主轴冷却水箱水位,低于下限则加注专用冷却液(型号 HFC-25);
(3)检查冷却泵是否运转,若泵不转,检查泵电机热继电器是否跳脱,复位后重启;
(4)若以上正常仍报警,检查主轴轴承,振动值超过 4.5mm/s 时联系设备科更换轴承,切勿强行开机。
2.2 加工尺寸超差
故障现象:加工孔位偏差超过 0.02mm。
处理步骤:
(1)检查工件装夹是否松动,重新装夹并对刀;
(2)检查丝杠反向间隙,超过 0.015mm 时在系统参数中做反向间隙补偿;
(3)检查环境温度,车间温度偏离 20℃±5℃ 范围时精度受影响,夏季高温时段建议安排在早晚加工高精度件。
2.3 气动夹具夹不紧
故障现象:夹紧压力显示正常但工件打滑。
处理步骤:(1)检查夹爪齿面磨损,磨损超标更换夹爪;(2)检查气路三联件滤芯,积水过多会顶开安全阀导致实际压力不足,排水并更换滤芯。
第三章 安全操作红线(严禁事项)
(1)严禁在主轴旋转时打开防护门;
(2)严禁戴手套操作机床;
(3)严禁擅自修改系统参数,参数修改必须由设备科授权工程师执行并记录;
(4)严禁冷却液溅入电柜,电柜门必须常闭锁紧;
(5)发生异响、焦糊味、冒烟时立即按急停并断开电柜总电源,撤离并上报班组长。
第四章 保养周期表
主轴轴承:每 6000 小时检查一次振动值,超过 4.5mm/s 更换;
丝杠润滑:每 500 小时加注锂基润滑脂;
冷却液更换:每 3 个月整体更换一次;
电柜除尘:每季度专业除尘一次;
精度校验:每半年做一次定位精度检测(激光干涉仪),重复定位精度标准 ±0.005mm。

附录 B:四题验收测试用例(回归验证标准)#

固定测试集,每改一处参数/代码就重测全部 4 题。3 题考”文档里有”,1 题考”文档里没有”(防幻觉)。

#测试问题预期正确回答考察点实测历史
1每日开机前要完成哪几项点检?5 项:液压油位、冷却液浓度、气压表、导轨润滑油泵、听音检查信息完整性(多要点并列)✅ 一次通过
2冷却液浓度低于多少需要补加原液?低于 4%(正常范围 5%-8%)精确数值检索❌→✅ 暴露切分器 bug(见第九章问题 B)
3主轴过热报警怎么处理?停机冷却 15 分钟→查水位加 HFC-25→查冷却泵热继电器→振动超 4.5mm/s 换轴承多步骤流程复述✅ 一次通过
4公司年假有几天?必须回答”知识库中没有相关内容”(文档无此信息)防幻觉:拒绝编造❌→✅ 曾编造”5 天”,阈值提至 0.45 后正确拒答(见第九章问题 A)

设计说明:第 2 题的”4%“故意埋在长段落中间,专测切分与召回质量;第 4 题是文档外的”陷阱题”,专测模型会不会编造。四题全绿 = RAG 全链路(解析→切分→向量化→检索→生成→拒答)验收通过。


笔记完 · 配套代码:qwen3-demo

从零到上线:RAG 知识库助手保姆级教程
https://www.cati.cc.cd/posts/qwenragzhushou/
作者
MIAOWU
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0

评论区

动作模组
等待模型加载…
看板娘
公告
公告大版本更新

一口气全面统一博客样式,移除部分冗余导航页面,更专注内容展示。

公告稳定性修复一批

统一Swup生命周期(BUG更少了、牺牲了首屏部分性能)、升级astro 7(构建速度翻倍)、文章封面图片加载(更流畅)、更换音乐播放器 API(更稳定)等多个问题,站点更稳了。

公告互换友链

欢迎各位大佬互换友链,要求内容原创、稳定更新。申请前请先看友链页的说明,期待和你交换链接。

查看详情
公告关于我的介绍

欢迎来到我的博客,我是深耕java、python和agent技术开发。热爱技术、持续学习,欢迎同好交流探讨。

查看详情
音乐
封面

音乐

暂未播放

0:00
0:00
暂无歌词

日历

正在加载日历...

--

    最近节日

    正在加载日历...
    ----

    最近生日/纪念日

    正在加载日历...
    ----
    距月底--
    距年底--

    隐私政策

    更新日期: 2026 年 7 月 15 日
    生效日期: 2026 年 7 月 15 日

    适用范围#

    本政策适用于 MiaoWu 的博客(以下简称“本站”)。本站是个人博客,用于发布和分享内容;不提供账户注册、支付、定位或广告投放服务。访问本站、发表文章评论或在留言板留言前,请阅读本政策。

    信息收集与使用#

    本站只在提供内容、评论和留言功能,以及维护站点安全所需的范围内处理信息。

    • 访问与统计信息:访问页面时,统计服务可能处理访问时间、页面地址、来源页、浏览器和设备相关信息,用于了解内容访问情况、排查故障和改进站点。
    • 评论信息:使用文章评论功能时,Waline 可能处理您主动提交的昵称、邮箱、站点链接和评论内容;还可能处理 IP 地址等必要信息,用于防止垃圾评论、滥用和维护服务安全。评论内容、昵称和站点链接(如填写)可能公开展示在文章下方;邮箱不会公开展示。
    • 留言信息:留言板使用 Waline /guestbook/ 频道。Waline 可能处理您主动提交的昵称、可选邮箱、站点链接、留言内容和图片,以及浏览器、操作系统、IP 地址等必要的反滥用信息。默认情况下,留言图片以内嵌数据随留言提交;如站点维护者配置了远程图片上传接口,图片会先发送至该接口并在留言中保存返回的图片地址。留言内容、昵称、图片和站点链接(如填写)可能公开展示;邮箱和 IP 地址不会在留言板公开展示。

    请不要在评论或留言中提交身份证件、银行卡、住址、密码或其他不必要的敏感个人信息。

    第三方服务#

    为实现本站功能,以下第三方会在各自服务范围内处理相关数据:

    • Umami:用于匿名化的网站访问统计和出站链接点击统计,帮助我了解本站的使用情况。
    • Waline:用于文章评论、留言板及访问量统计。服务会按照其自身规则处理您在评论或留言时提交的信息及必要的反滥用信息。
    • Cloudflare:可为本站提供静态资源分发和域名基础设施。
    • unpkg:用于加载 Waline 的前端脚本、样式和表情资源;请求这些资源时,您的浏览器会与该服务建立连接。

    第三方服务可能有独立的隐私政策和数据保存规则。请在使用相关功能前查阅其规则;本站无法控制其独立的数据处理活动。

    本站主要使用浏览器本地存储(Local Storage 或 Session Storage)保存使用偏好,例如主题颜色、明暗模式、文章列表视图和音乐播放设置。留言板会在本地保存匿名资料、未发送草稿和登录状态,以便恢复输入与会话;管理员登录状态仅保存在当前会话。

    本站不主动设置用于广告定向的第一方 Cookie。评论、统计或资源服务可能按照其自身规则使用 Cookie 或类似技术。您可以通过浏览器设置查看、删除或限制 Cookie 和本地存储;清除后,部分偏好或互动状态可能会恢复为默认值,评论功能也可能受到影响。

    信息公开、保存与安全#

    评论和留言属于公开互动内容,提交后可能被搜索引擎收录、被他人引用或在缓存中短暂保留。请谨慎决定发布内容。除非您提出删除请求、内容违反规则或法律法规另有要求,公开内容会持续保留以维持讨论上下文。

    本站会采取合理措施保护数据安全,包括使用 HTTPS、输入校验、内容转义和访问频率限制。但互联网传输和第三方服务均无法保证绝对安全,请理解并自行承担公开发布信息的相应风险。

    你的权利#

    你可以通过 996491835@qq.com 联系我,申请查询、更正或删除由本站直接保存的评论、留言或相关公开内容。为保护他人权益,请在请求中提供足以定位内容的信息,并说明你与该内容的关系;必要时可能需要进行合理核验。

    对于由 Waline、Umami、Cloudflare 或 unpkg 独立处理的数据,你也可以直接向对应服务提供方行使相关权利。删除公开评论或留言后,第三方缓存、搜索引擎索引或他人转载的副本可能无法立即同步删除。

    未成年人条款#

    未满 14 周岁的未成年人应在监护人同意和指导下使用本站的评论、留言等互动功能。监护人如发现未成年人未经同意提交了个人信息,可通过上述联系方式与我联系,我会在合理范围内协助处理。

    政策更新与联系#

    我可能因本站功能或适用规则变化更新本政策,更新后的版本将在本站公布并标明日期。继续使用相关功能即表示你已阅读并理解更新后的政策。

    如对本政策或数据处理有疑问,请联系 996491835@qq.com。

    用户协议

    更新日期: 2026 年 5 月 19 日
    生效日期: 2026 年 5 月 19 日

    适用范围#

    本协议适用于你访问 MiaoWu 的博客,以及使用文章评论、留言板等互动功能的行为。继续浏览本站或提交评论、留言,即表示你已阅读、理解并同意遵守本协议及本站的隐私政策。

    评论及留言规则#

    请在交流中保持友善、理性和尊重。你不得利用本站发布、传播或实施以下行为:

    • 发布任何违反中华人民共和国法律法规的内容。
    • 发布任何侵犯他人合法权益的内容,包括但不限于隐私、名誉、肖像、著作权、商标权和其他知识产权。
    • 恶意攻击、辱骂、骚扰、威胁、歧视其他用户或任何第三方。
    • 发布垃圾广告、恶意推广、刷屏、灌水,或与讨论主题明显无关的重复内容。
    • 利用本站进行网络诈骗、钓鱼、传播恶意软件,或发布可能危害网络和信息安全的内容。
    • 绕越或试图绕越本站的审核、限流、封禁等管理措施。
    • 冒充他人、伪造身份,或收集、公开他人的个人信息。

    内容与访问管理#

    你应对自己发布的评论和留言负责,并保证拥有发布该等内容所需的合法权利。论坛管理员有权在不另行通知的情况下删除违规内容、限制或封禁违规账号,或限制其继续使用本站互动功能。

    如发现涉嫌违法犯罪、严重侵权或危及本站安全的内容,本站可保留相关记录,并在法律法规要求或必要时向有关部门提供协助。对管理措施有疑问时,可通过文末联系方式说明情况;本站会结合实际情况处理,但不承诺恢复已删除内容或访问权限。

    知识产权与内容授权#

    本站原创文章、页面设计和其他受保护内容的权利归作者或权利人所有。未经授权,请勿复制、转载、镜像或用于商业用途;法律法规允许的合理使用除外。

    你发布评论或留言时,授予本站为展示、存储、备份、审核、删除和维护互动功能所必需的非独占、免费的使用许可。该许可不改变你对原创内容依法享有的权利。

    免责声明#

    本站内容仅用于个人记录、学习交流和一般信息参考,不构成任何专业意见、承诺或担保。你应结合自身情况独立判断,并对据此采取的行动负责。

    评论、留言和外部链接中的内容由其发布者或运营者负责,不代表本站立场。本站会在合理范围内处理明显违规内容,但不保证所有内容均及时发现,也不对第三方网站的可用性、内容、安全性或隐私实践承担责任。

    因网络故障、不可抗力、第三方服务异常、维护升级或超出合理控制范围的原因导致本站暂时无法访问、内容延迟或数据丢失的,本站会尽力恢复,但不承担由此产生的间接损失。

    未成年人条款#

    未满 14 周岁的未成年人应在监护人同意和指导下使用评论、留言等互动功能。监护人应协助未成年人理解本协议,并对其使用行为进行必要的引导。

    其他条款#

    我可以根据本站功能、管理需要或法律法规变化更新本协议,更新后的版本将在本站公布并标明日期。继续使用本站即视为接受更新后的协议。

    本协议的订立、执行和解释适用中华人民共和国法律。因本协议或使用本站产生争议时,双方应先友好协商;协商不成的,依法向有管辖权的人民法院解决。

    如对本协议或内容管理有疑问,请联系 996491835@qq.com。

    复制成功,转载请标注本文地址