# ip_fqa_rag **Repository Path**: duydeckle/ip_fqa_rag ## Basic Information - **Project Name**: ip_fqa_rag - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-22 - **Last Updated**: 2026-10-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 知识产权智能问答(ip_system) 本文件给 AI 编码助手(以及新加入的同学)提供 **ip_system 子系统** 的项目上下文。 **改代码前请先读完第 5、6、11 节**:那里每一条都是这套栈上真实会踩的坑。 想先理解"为什么这么设计、要解决什么业务痛点",读第 0 节。 > 本文件描述知识产权(专利 / 商标 / 著作权)问答系统 `ip_system/` 的规范与项目上下文。 > `ip_system/` 是**独立可搬迁的子系统**:自带 `requirements.txt`、`.env`、`web/` 前端与 `tests/`, > 不 import 本目录之外的任何代码,只共用 Docker 服务(MySQL / Redis / Milvus)与 conda 环境。 --- ## 0. 项目背景与痛点 ### 0.1 背景:为什么需要一个"知识产权专用"的问答系统 通用大模型能把知识产权"聊"得像模像样,但业务上不能直接用,因为它缺两样东西:**依据**(说的话出自哪一条)、**边界**(哪些话不能说)。 而知识产权咨询的工作量分布极其集中:专利法规查询、专利撰写咨询、检索分析、商标注册、维权诉讼这五类占了绝大多数提问(见第 1 节的意图表),每一类都对应固定的知识源——专利法 / 实施细则 / 审查指南 / 商标法 / 分类表 / 司法解释 / 判例。这意味着**它不是开放域问答,而是一个"窄域、深知识、强依据"的场景**,正好适合"分层拦截 + 检索增强"的工程解法,而不是把一切都丢给大模型。 为什么不做领域微调(LoRA):法规和审查指南更新很勤(专利法 2020 修正、实施细则 2023 修订、商标法 2019 修正、著作权法 2020 修正、审查指南年年改),微调一轮要几周且每次都要重评;**领域味靠检索语料 + `pipeline/prompts.py` 一天就能跟上**。所以本项目把预算花在"语料质量、引用校验、缓存分层"上,而不是花在训练上。 一句话定位:**用最便宜的一层挡住绝大多数问题(问候 → BERT 意图 → 三级缓存),只把真正需要推理的那部分送去"检索 + 生成",并且保证每一句依据都能点回法条。** ### 0.2 业务侧的痛点(这些决定了功能长什么样) | # | 痛点 | 真实表现 | 后果 | 本系统的应对 | |---|---|---|---|---| | 1 | **引用不可追溯** | 模型输出"根据专利法相关规定"或编一个不存在的条号 | 无法核对,错了没人发现 | 上下文块强制编号【来源 n】,答案句末标〔来源 n〕,生成后做**越界编号剔除**(第 6.5 节、红线 3) | | 2 | **不能给"法律意见"式结论** | 模型爱说"这个方案一定能授权""这官司能赢" | 合规与执业风险 | 提示词写死"只给依据 + 适用条件 + 建议",答案末尾固定免责声明(红线 5) | | 3 | **高频问题重复、长尾问题稀少** | 80% 的提问是那几十个问题的变体,剩下的才需要检索推理 | 全走大模型又贵又慢(首字 3s+) | 三级缓存(Redis → MySQL FAQ → BM25)先拦,命中 3ms 返回 | | 4 | **多轮省略导致检索不到** | "那权利要求呢?""这个还能注册吗?" | 短句缺上文,检索/分类都失准 | MySQL 存最近 5 轮 + 分类与检索前做**查询改写**(第 6.6 节) | | 5 | **答复口径不统一** | 不同代理师对同一问题给不同说法 | 客户体验与责任归属混乱 | 高频问题沉淀成 FAQ 标准答案(带来源),统一出口径 | | 6 | **敏感信息外发风险** | 用户把技术交底书、商标图样、客户名单贴进问题 | 送云端 API 即泄密 | 送模型前做 PII 脱敏(`pipeline/sanitizer.py`);系统不接收文件上传(红线 4) | ### 0.3 工程侧的痛点(这套技术栈上真实踩过的) | # | 痛点 | 表现 | 本系统怎么解 | |---|---|---|---| | 1 | **短句被判成"通用知识"就不检索** | "权利要求书怎么写"被分类成闲聊,直接走通用问答,答得泛泛 | BERT 五分类 + 置信度阈值 + 关键词/域名词兜底(第 6.2 节) | | 2 | **FAQ 近似匹配"泛词撞车"** | 实测"我能强行注册别人注册过的商标吗"命中"马德里国际注册怎么提交",答案完全错 | 光有分数阈值不够:加**词覆盖门限**(覆盖率 + 共用实词数)+ **L1 缓存自愈校验**(`faq/bm25.py`) | | 3 | **首字太慢,前端像挂了** | Direct 约 2.6s、SubQuery 约 4.5s(检索前还要多调 LLM) | 分层拦截把大多数请求挡在检索之前;`meta` 先到让前端立刻有反馈;lifespan 预热模型(第 11 节坑 6) | | 4 | **缓存会把一次错误固化** | 拒答话术被缓存 24 小时;改完语料旧答案还在吐 | 拒答/兜底不进缓存与历史(红线 1);语料变更走"清缓存 → 重灌 → 重建快照"三步(坑 16) | | 5 | **云端限流/超时给出半截答案** | 429 或网络抖动,答案只出一半就断了 | 超时 60s + 重试 1 次,仍失败走兜底话术;**只有 `done` 之后才落库**(坑 8、15) | | 6 | **客户端断开留下脏数据** | 用户关页面了,后端还在写历史,历史里出现半截答案 | 捕获 `WebSocketDisconnect` → 取消任务 → 半截答案丢弃(坑 15) | | 7 | **7G 模型冷启动** | 首次请求要加载 BGE-M3 + Reranker + BERT,几十秒不响应 | 意图模型同步预热、向量模型后台预热;前端保留"模型加载中"(坑 6) | ### 0.4 三条不可让步的底线(都能机器校验,不是口号) 1. 答案里的每一个〔来源 n〕都必须能在**本轮** citations 里找到 n,找不到就剔除(红线 3) 2. 拒答 / 兜底 / 异常话术**不进缓存、不进历史**(红线 1) 3. 不给"一定能授权 / 一定能胜诉"式结论,免责声明固定在提示词末尾(红线 5) 这三条对应第 10 节"红线"里可自动化的部分;第 5 节表格里每一行都写了"失败 / 降级"怎么兜,改流程时不要绕开它们。 ### 0.5 用数字说话(当前版本实测,换语料 / 换模型后必须重测) | 指标 | 实测值 | 出处 | |---|---|---| | 意图五分类准确率 | golden set **92.00%**,验证集 92.73% | `tests/eval_intent.py` | | FAQ 三级缓存 | L1 命中 **3ms**、L2 155ms、L3 命中需过词覆盖门限 | `python -m faq.bm25` | | 检索 recall@5 | **0.8696**、MRR@5 0.7393 | `tests/eval_recall.py` | | 答案首字 | Direct 2.6s / SubQuery 4.5s / Backtrack 4.4s | 见 `NOTES.md` 第 3.4 节 | | 引用越界编号 | 正常问答 0 个越界,异常情况全部剔除并打 WARNING | `logs/ip.log` 关键字 `引用=` | 完整实测数据、标定过程与已知限制见 **`NOTES.md`**;那里面每一条都对应本文件某条红线或坑,出问题时先查它。 --- ## 1. 项目是什么 `ip_system` 是一个**面向知识产权业务的分层问答系统**:先用规则挡掉寒暄、再用 BERT 判意图、再用三级缓存拦掉大部分重复问题,只有真正需要推理的问题才走"检索增强 + DeepSeek flash 生成",最后通过 WebSocket 逐 token 推给前端,并**带法条级引用**。 本系统的关键取舍(每一项都对应下面某条坑或红线): | 维度 | 选择 | 为什么这么选 | |---|---|---| | 意图粒度 | BERT 五分类(专利法规查询 / 专利撰写咨询 / 检索分析 / 商标注册 / 维权诉讼,**本地微调推理**) | 五类闭集、输入 10~30 字,单条前向 10ms 级,CPU 也能跑;输出是概率分布,可直接拿置信度决定是否回落关键词 | | 生成模型 | **DeepSeek flash 云端 API**(OpenAI 兼容接口) | 不需要 28G GPU 与自建推理服务;领域味靠语料 + 提示词,不靠微调 | | 前端 | WebSocket + 网页(引用卡片 + 流式渲染) | 引用必须能点回原文,CLI 打印做不到 | | 会话历史 | MySQL(最近 5 轮) | 要按 `session_id` 查询与审计,Redis list 不适合 | | 输出要求 | **必须带法条 / 文档引用 + 免责声明** | 业务硬要求:不能给"法律意见"式结论(红线 5) | **五个业务意图的边界**(判意图、写提示词、做评测集都以这张表为准): | 意图 | 典型问题 | 主要知识源 | 默认策略 | |---|---|---|---| | 专利法规查询 | "专利法第二十二条讲什么"、"职务发明的权属归谁" | 专利法、实施细则、审查指南 | Direct | | 专利撰写咨询 | "权利要求书怎么写才能覆盖竞品"、"说明书公开到什么程度" | 审查指南第二部分、撰写规范、案例 | HyDE / Backtrack | | 检索分析 | "帮我分析这个技术方案的新颖性"、"FTO 检索怎么做" | 检索报告、分类号体系、判例 | SubQuery | | 商标注册 | "第 35 类能不能注册"、"商标被驳回怎么复审" | 商标法、分类表、审查标准 | Direct | | 维权诉讼 | "被人仿冒外观设计怎么取证"、"侵权赔偿怎么算" | 专利法/商标法侵权条款、司法解释、判例 | Backtrack / SubQuery | --- ## 2. 技术栈 意图走 BERT 五分类(闭集短文本、CPU 可跑),生成用 DeepSeek flash(无需自建推理), 其余各层选择与理由如下: | 层 | 选型 | 说明 | |---|---|---| | 语言 / 环境 | Python 3.12,conda 环境 `edu_rag` | 复用已有环境,不再新建 | | 编排框架 | LangChain 1.x(`langchain-core` / `langchain-openai`) | 只用链式能力,不用 Agent | | 生成模型 | **DeepSeek flash**,官方 OpenAI 兼容接口 `https://api.deepseek.com/v1` | 全项目唯一一个 LLM 客户端(`pipeline/generator.py`) | | 领域适配 | 提示词 + 引用校验(**不用 LoRA**),领域味靠检索语料与 `pipeline/prompts.py` | 法规更新频繁,语料+提示词一天就能跟上 | | 意图模型 | **BERT 五分类**(`bert-base-chinese` 微调,本地推理) | 离线训练,线上只做前向 | | 策略路由 | DeepSeek flash(与生成共用客户端,低温 + 小 `max_tokens`) | 只输出 4 个枚举值之一 | | 向量模型 | `BAAI/bge-m3`(dense 1024 维 + sparse 学习型稀疏) | 一路 dense、一路 sparse,混合检索 | | 精排 | `BAAI/bge-reranker-base`(CrossEncoder) | 父块重排,分数用于"检索没找到依据"的降级判断 | | 向量库 | Milvus 2.4(HNSW/IP + SPARSE_INVERTED_INDEX,WeightedRanker 0.7/0.3) | schema 创建时固化,改结构必须重建(坑 10) | | 缓存 | Redis 8(三级缓存的 L1) | 键一律 `ip:` 前缀,`clear_cache()` 靠前缀清理 | | 关系库 | MySQL 8.4(FAQ 语料 + 会话历史 + 引用记录 + 知识库台账) | 四张表见第 7 节 | | 分词 / 关键词 | jieba(**必须加 IP 领域自定义词典**)+ rank-bm25 | 不加词典 BM25 召不回专业问题(坑 11) | | 接入层 | FastAPI + `uvicorn[standard]` + 原生 WebSocket | 同步生成器必须走线程池桥接(坑 7) | | 配置 | `pydantic-settings`,唯一配置源 `base/config.py` | 代码里只读 `config.XXX`,禁止 `os.getenv` | **依赖**:本子系统自己的依赖全写在 `requirements.txt` 里,不依赖外部其他清单: `fastapi`、`uvicorn[standard]`、`websockets`、`sentence-transformers`、`torch`、`transformers`; 训练 BERT 意图模型时另需 `datasets`、`scikit-learn`(仅训练阶段,运行期不需要)。 --- ## 3. 目录地图(目标形态) ``` ip_system/ CLAUDE.md # 本文件 requirements.txt # 本子系统自己的依赖清单(不依赖外部清单) main.py # CLI 入口(开发期调试用) (无根 __init__.py) # 顶层即包根:base/ app/ pipeline/ ... 都是顶层包(坑 2b) base/ config.py # 唯一配置源(第 7 节的字段全在这里) logger.py # 日志配置,落 logs/ip.log app/ server.py # FastAPI + WebSocket 端点 + 生命周期(模型预热) schemas.py # 请求/事件 pydantic 模型(第 9 节协议) deps.py # 全局单例装配(管线、Milvus、MySQL、Redis) pipeline/ orchestrator.py # 总编排:问候 → 意图 → 三级缓存 → 策略 → 检索 → 生成 → 流式 greeting.py # 问候语检测(纯规则,毫秒级,不调模型) intent.py # 意图识别(BERT 五分类推理 + 置信度阈值 + 关键词兜底 + 缓存) intent_train.py # BERT 五分类微调脚本(离线训练,产出 models/intent_bert/) cache.py # 三级缓存:Redis → MySQL FAQ → BM25 strategies.py # Direct / HyDE / SubQuery / Backtrack 四种检索策略 router.py # 意图 → 默认策略的路由表 + LLM 覆盖 + 降级 generator.py # 提示词组装、流式生成、引用编号校验 prompts.py # **全部提示词只在这里**(变量契约 context/query/history) history.py # MySQL 会话历史读写 + 多轮查询改写 store/ milvus.py # collection schema、混合检索、父块映射高亮、精排 mysql.py # FAQ / chat_message / citation / kb_doc 四张表 redis_client.py # 键前缀、TTL、语料快照 faq/ bm25.py # BM25 检索(阈值重标定) tokenizer.py # jieba + IP 自定义词典 + 停用词 knowledge/ faq/ # FAQ CSV(列:intent, question, answer, source, updated_at) docs/ # 法规原文、审查指南、判例、内部答复模板 loaders/ # 多格式加载工厂(PDF/DOCX/PPTX/图片 OCR/Markdown) splitters/ # 语义切分器(BGE-M3,供超长非结构化文档使用) ingest.py # 离线入库:加载 → 切分 → 编码 → upsert models/ intent_bert/ # BERT 五分类权重 + tokenizer + label2id.json(不入库) web/ # 前端静态页(单文件 HTML 亦可),引用卡片 + 流式渲染 tests/ golden_set.jsonl # 评测集(每行:query / 期望意图 / 期望引用文档) intent_train.jsonl # BERT 五分类训练/验证集(query, label),label 必须取自 INTENT_LABELS ws_smoke.py # WebSocket 冒烟脚本 eval_intent.py # 意图准确率 / 混淆矩阵 eval_recall.py # recall@5 / 引用准确率 logs/ # 运行期生成,不入库 .env # 不入库 ``` --- ## 4. 模块职责与边界 `ip_system` 是**独立可搬迁的子系统**:自带配置、依赖、前端与测试,不 import 本目录之外的代码。 下面这张表是"哪件事该改哪个文件"的索引——改错层是这套系统最常见的返工原因。 | 模块 | 一句话职责 | 不要在这里做的事 | |---|---|---| | `base/config.py` | 唯一配置源(第 6 节字段全在这) | 读 `os.getenv`、拼相对路径 | | `base/logger.py` | 日志落 `logs/ip.log` | 库代码里 `print` | | `app/server.py` | FastAPI + `/ws/chat` + 生命周期预热 | 在协程里直接跑同步模型/DB 调用(坑 7) | | `app/schemas.py` | WS / HTTP 的 pydantic 契约(第 8 节协议) | 放业务逻辑 | | `app/deps.py` | 全局单例装配(编排器、Milvus、MySQL、Redis) | 每次请求重新加载 7G 模型 | | `pipeline/orchestrator.py` | 总编排:问候 → 意图 → 缓存 → 策略 → 检索 → 生成 → 推送 | 在这里拼提示词或写 SQL | | `pipeline/greeting.py` | 问候语规则拦截(毫秒级) | 调模型判断寒暄 | | `pipeline/intent.py` | BERT 五分类推理 + 置信度阈值 + 关键词兜底 | 线上调 LLM 做意图 | | `pipeline/intent_train.py` | 离线微调,产出 `models/intent_bert/` | 服务启动时训练 | | `pipeline/cache.py` | 三级缓存编排(L1 Redis → L2 MySQL → L3 BM25) | 在这里做检索或生成 | | `pipeline/router.py` | 意图 → 策略(默认路由表 + LLM 覆盖 + 兜底) | 让解析异常冒泡到主流程 | | `pipeline/strategies.py` | 四种检索策略;高级策略空召回降级 Direct | 直接拼提示词 | | `pipeline/generator.py` | LLM 客户端(全项目唯一实例)+ 流式生成 + 引用校验 | 新建第二个 LLM 客户端 | | `pipeline/prompts.py` | **全部提示词模板**(变量契约 `{context}` `{query}` `{history}`) | 业务代码里拼长模板 | | `pipeline/history.py` | MySQL 会话历史(最近 5 轮)+ 多轮查询改写 | 把会话历史塞回 Redis | | `pipeline/sanitizer.py` | 送云端前的 PII 脱敏 | 拿它当权限控制 | | `store/milvus.py` | `ip_knowledge` 集合:写入、混合检索、父块映射、精排 | 改 schema 不重建集合(坑 10) | | `store/mysql.py` | `faq` / `chat_message` / `citation` / `kb_doc` 四张表 | 在业务层写裸 SQL | | `store/redis_client.py` | `ip:` 前缀键、TTL、语料快照、`clear_cache()` | 存会话历史 | | `faq/tokenizer.py` | jieba + IP 自定义词典 + 停用词 + 查询归一化 | 直接改 jieba 默认词典 | | `faq/bm25.py` | L3 关键词召回 + 词覆盖门限 + 阈值标定 | 在这里调 LLM | | `knowledge/ingest.py` | 离线入库:加载 → 切分 → 编码 → upsert + 台账 | 在服务运行期调用 | | `knowledge/loaders/` `splitters/` | 多格式加载与切分(PDF/DOCX/图片 OCR/语义切分) | 在这里做业务判断 | | `web/index.html` | 单文件前端(流式渲染 + 引用卡片) | 用 `innerHTML` 直插模型输出 | | `tests/*` | 评测与冒烟脚本 | 有副作用的业务入口 | 一句话记法:**`base` 管配置、`app` 管接入、`pipeline` 管流程、`store` 管存储、`faq` + `knowledge` 管语料、`web` 管展示。** --- ## 5. 业务流程(与流程图一一对应) ``` 用户输入 └─① 问候语检测(规则)──命中──> 直接回寒暄 + 引导语,不调模型 └─② 意图识别(BERT 五分类 + 置信度阈值 + 关键词兜底) └─③ 三级缓存:Redis ──> MySQL FAQ ──> BM25 └─命中──> 返回 FAQ 标准答案(带来源标注) └─④ 策略选择器(意图默认路由 + LLM 动态路由) └─⑤ 四选一检索:Direct / HyDE / SubQuery / Backtrack └─⑥ Milvus 混合检索(BGE-M3 dense+sparse,Top-K 5)→ 父块映射 → 精排 └─⑦ 对话历史拼接(MySQL,最近 5 轮) └─⑧ LLM 生成(DeepSeek flash,流式) └─⑨ WebSocket 逐 token 推送 └─⑩ 前端展示 + 引用来源 + 知识产权资料卡片 ``` | 步骤 | 输入 → 输出 | 失败 / 降级 | |---|---|---| | ① 问候检测 | 原文 → `bool` | 规则表为空 → 视为非问候,继续 ②;此步**禁止**调模型 | | ② 意图识别 | 原文(+改写后的完整问题)→ 五分类之一 | 模型未加载 / 置信度 < `INTENT_CONFIDENCE_THRESHOLD` → 关键词兜底 → 仍失败则 `通用咨询` 走 RAG Direct | | ③ 三级缓存 | 原文 + 意图 → `answer + source` | Redis 挂 → 跳过;MySQL 挂 → 只走 BM25;BM25 语料空 → 全部当未命中 | | ④ 策略路由 | 意图 + 原文 → `DIRECT/HYDE/SUB_QUERY/BACKTRACK` | DeepSeek 输出无法解析 / 超时 → 用意图默认路由(**绝不允许异常冒泡**) | | ⑤ 检索 | 策略 → `list[Document]` | 高级策略召回为空 → 自动降级 Direct(必须打 WARNING 日志) | | ⑥ 检索收敛 | 候选池 → 2~3 条父块 | 全空 → 不带上下文生成,并在答案里声明"知识库未覆盖" | | ⑦ 历史拼接 | session_id → 最近 10 条消息文本 | MySQL 失败 → 空历史,不影响本轮 | | ⑧ 生成 | 上下文 + 历史 + 问题 → 流 | 异常 → 兜底话术(含人工服务入口),**不写缓存、不写历史** | | ⑨ 推送 | 事件流 → WebSocket | 客户端断开 → 取消生成任务,丢弃半截答案 | --- ## 6. 关键设计决策(改之前先理解为什么) ### 6.1 为什么意图用 BERT、生成用 DeepSeek flash 1. **意图是闭集短文本分类,BERT 更合适**:五类标签固定、输入通常 10~30 字,`bert-base-chinese` 微调后单条前向 10ms 级,CPU 也能跑;输出天然是概率分布,可直接用置信度阈值决定是否回落关键词规则。 2. **生成交给云端 DeepSeek flash**:不需要 28GB GPU,也不需要维护 vLLM 部署;引用格式由 `prompts.py` 固定 + 生成后编号校验兜底,不再依赖 LoRA 固化输出习惯。 3. **代价与风险**:DeepSeek 是第三方云 API,技术交底书、商标图样、客户名单存在外发风险 → 必须先脱敏(隐去申请人、发明人、客户名与未公开技术细节),并遵守第 10 节红线 4。 4. **BERT 的代价**:需要标注数据(每类 ≥ 200 条)并随业务迭代重训;对短句和 OOD 泛化弱,所以关键词兜底与低置信度回落不能省。 ### 6.2 意图分类:BERT 五分类 + 关键词兜底 - 五类标签是**业务契约**,写在 `config.INTENT_LABELS` 里;训练时的 `label2id` 必须与它**同序**(顺序错位会让五类整体串位,见第 11 节坑 9)。 - 模型:`bert-base-chinese` 微调,`[CLS]` 接 5 类 softmax;训练脚本 `pipeline/intent_train.py`,权重与 tokenizer 落 `models/intent_bert/`(不入库)。 - 推理:本地前向取 `argmax` + `confidence`;`confidence < INTENT_CONFIDENCE_THRESHOLD` 视为不可信,进入关键词兜底;模型加载失败同样走兜底,**不要把异常抛给主流程**。 - **关键词兜底**:出现"专利/商标/著作权/版权/侵权/驳回/复审/无效/权利要求/分类号"等词但模型判成低置信 / "通用咨询"时,按关键词强判。这是"短句被判成通用知识、不走检索"这个头号问题的直接修复。 - 分类结果按 `(query_hash)` 缓存 1 天:BERT 本身很快,缓存主要给高并发与批量评测省算力,`INTENT_CACHE_TTL` 可按需调小。 - 冷启动标注数据不足时,用 DeepSeek flash 对无标注 query 做**离线预标注**,必须人工复核后再进训练集;**线上不调用 LLM 做意图分类**。 ### 6.3 三级缓存:顺序、键、边界 - 顺序固定 **Redis(L1) → MySQL FAQ(L2) → BM25(L3)**,前一级命中就不再往下走;L3 命中后要**回填** L1(否则每次都要算 BM25)。 - 键约定:`ip:faq:ans:{md5(query)}`、`ip:faq:corpus:original`、`ip:faq:corpus:tokenized`、`ip:ans:{md5(query)}`(RAG 答案)、`ip:session:{id}:flags`。 - 归一化后再算 md5:去首尾空白、全角转半角、英文小写、去掉"请问/一下/呢"这类口头词。**否则"专利怎么申请?"和"专利怎么申请"会各占一条缓存。** - **绝对不进缓存**:拒答 / 兜底话术 / 信息不足 / 异常提示。踩过这个坑:一次失败被固化 24 小时。 - **多轮(history 非空)不读按 query 的 RAG 缓存**:同一句话在不同上下文里答案不同。 - FAQ 语料变更后必须 `clear_cache()` + 重新灌 L2/L3 快照,否则 Redis 里的旧语料会把系统锁死在 7 天前。 ### 6.4 "意图驱动 + 动态路由" 两层: 1. **默认路由表**(`router.py`)按第 1 节的表格给每个意图一个默认策略 —— 这是业务知识,改它要评审。 2. **LLM 覆盖**:在默认策略明显不合适时允许换(例如"专利法规查询"里出现"帮我比较中美的宽限期规定" → SubQuery)。 3. 无论哪层,**最终必须落到四个枚举值之一**;LLM 输出走"白名单 + 正则提取 + 默认值"三重保险,解析失败只记日志不抛异常。 ### 6.5 引用(citation)是第一等公民 - 每个上下文块进提示词前必须带编号:`【来源 1】《专利法》(2020 修正)第二十二条 …`。 - 模型输出要求:**句末标注 `〔来源 n〕`**;生成结束后做一次引用校验——答案里出现的编号必须存在于本轮 citations 中,越界编号直接剔除(防止模型编造来源)。 - 返回给前端的 citations 数组顺序**必须**与提示词里的编号一致,否则前端卡片会错位。 - 每条引用至少带:`index / title / article / effective_date / snippet / score`。 - **免责声明**写死在提示词末尾的固定段落,不依赖模型自由发挥:"以上内容基于现行法规整理,不构成法律意见,具体案件请咨询专利代理师或律师。" - 法条时效:`kb_doc` 表必须存 `version` 与 `effective_date`,引用时一并返回。专利法 2020 修正、商标法 2019 修正、著作权法 2020 修正,答案里不要出现"旧法"表述。 ### 6.6 会话历史 - 存 MySQL `chat_message`,读最近 `HISTORY_MAX_TURNS=5` 轮(10 条);按 `session_id` 建索引。 - 拒答不进历史(模型看到自己之前的拒答会继续拒答)。 - **多轮查询改写**:分类与检索前先用 DeepSeek flash 把"那权利要求呢?""这个还能注册吗?"改写成完整问题(实测:不改写时 rerank 0.27,改写后 0.99)。改写失败就用原文,不要阻塞主流程。 ### 6.7 流式 - 同步 LangChain 生成器**不能**直接跑在 FastAPI 的协程里(会阻塞事件循环,表现为"整个服务卡住")。用 `anyio.to_thread.run_sync` / `run_in_threadpool` 桥接,或直接使用 DeepSeek 的 `AsyncOpenAI` 客户端。 - 推送按 **token 攒批**(约 20 字或 50ms 一批)发送,逐字符发会打爆 WebSocket 帧率。 - 生成过程中客户端断开:捕获 `WebSocketDisconnect` → 取消任务 → 不写缓存、不写历史。 --- ## 7. 配置(`base/config.py` 的全部字段) 唯一配置源,读 `ip_system/.env`;优先级:显式参数 > 环境变量 > `.env` > 默认值。**代码里读 `config.XXX`,不要 `os.getenv`。** ```python # ---- DeepSeek(云端 OpenAI 兼容) ---- DEEPSEEK_BASE_URL: str = 'https://api.deepseek.com/v1' DEEPSEEK_API_KEY: str = '' # 只从 .env 读,禁止硬编码 DEEPSEEK_MODEL: str = 'deepseek-flash' # 生成 / 改写 / 策略路由共用;以 /v1/models 返回名为准 DEEPSEEK_TEMPERATURE: float = 0.2 DEEPSEEK_MAX_TOKENS: int = 2048 DEEPSEEK_TIMEOUT: int = 60 # 云端首 token 受网络影响,别设 10s # ---- 意图(BERT 五分类,本地推理) ---- INTENT_MODEL_PATH: str = str(BASE_DIR / 'models' / 'intent_bert') INTENT_MAX_LEN: int = 128 INTENT_BATCH_SIZE: int = 32 INTENT_CONFIDENCE_THRESHOLD: float = 0.60 # 低于此值走关键词兜底 INTENT_DEVICE: str = 'cpu' # 有 GPU 时可改 'cuda' # ---- 检索 ---- TOP_K: int = 5 # 流程图 Top-K: 5(每路召回) CANDIDATE_M: int = 2 # 精排后进提示词的父块数(2~3) RERANK_ENABLED: bool = True HYBRID_DENSE_WEIGHT: float = 0.7 HYBRID_SPARSE_WEIGHT: float = 0.3 # ---- 意图与 FAQ ---- INTENT_LABELS: list[str] = ['专利法规查询', '专利撰写咨询', '检索分析', '商标注册', '维权诉讼'] INTENT_FALLBACK_LABEL: str = '通用咨询' # 仅低置信度/关键词兜底时使用,不在 BERT 五类里 FAQ_MIN_SCORE: float = 8.0 # BM25 原始分,换语料必须重新标定 FAQ_INTENT_STRICT: bool = False # True 时只用同意图的 FAQ 语料召回 # ---- 缓存与历史 ---- CACHE_KEY_PREFIX: str = 'ip' FAQ_CACHE_TTL: int = 7 * 24 * 3600 RAG_CACHE_TTL: int = 24 * 3600 INTENT_CACHE_TTL: int = 24 * 3600 HISTORY_MAX_TURNS: int = 5 # ---- 接入层 ---- WS_HEARTBEAT_SEC: int = 20 WS_TOKEN_BATCH_CHARS: int = 20 CORS_ORIGINS: list[str] = ['http://localhost:5173'] ``` `BASE_DIR` 必须由 `Path(__file__).resolve().parent.parent` 推导(ip_system 根),**不要**用相对路径或 `os.getcwd()`。 --- ## 8. 数据模型 ### MySQL(库名 `iprag`) ```sql faq(id, intent, question, answer, source, updated_at) -- 标准问答,L2 命中源 chat_message(id, session_id, role, content, intent, created_at) -- role: user / assistant citation(id, message_id, doc_id, title, article, snippet, score) -- 每条答案的引用快照 kb_doc(doc_id, title, doc_type, issuer, version, effective_date, file_path, checksum, ingested_at) -- 知识库文档台账 ``` ### Milvus(collection `ip_knowledge`) | 字段 | 类型 | 说明 | |---|---|---| | `id` | VARCHAR(128) PK | 子块 ID:`doc_{hash}_parent_{j}_child_{k}` | | `text` | VARCHAR(65535) | 子块原文(用于召回与高亮) | | `dense_vector` | FLOAT_VECTOR(1024) | BGE-M3 dense | | `sparse_vector` | SPARSE_FLOAT_VECTOR | BGE-M3 sparse | | `parent_id` / `parent_content` | VARCHAR | 父块映射(提示词里给父块,不给子块) | | `doc_id` / `title` / `article` | VARCHAR | 引用所需的最小元数据 | | `effective_date` / `version` | VARCHAR | 法条时效 | | `timestamp` | VARCHAR(50) | 入库时间 | 索引:dense 用 HNSW(`M=16, efConstruction=200, metric=IP`,检索 `ef=64`),sparse 用 `SPARSE_INVERTED_INDEX`(`IP`)。 ### Redis 键 `ip:faq:ans:{md5}`、`ip:faq:corpus:original`、`ip:faq:corpus:tokenized`、`ip:ans:{md5}`、`ip:intent:{md5}`。 全部键必须带 `ip:` 前缀(`clear_cache()` 靠前缀批量清理)。 --- ## 9. WebSocket 协议 端点:`/ws/chat`(文本帧 JSON,UTF-8)。 客户端 → 服务端: ```json {"type": "chat", "session_id": "u_123", "query": "专利被驳回怎么复审?", "top_k": 5} ``` 服务端 → 客户端(严格按顺序): ```json {"type": "meta", "session_id": "u_123", "intent": "专利法规查询", "strategy": "Direct", "from_cache": false} {"type": "citations", "items": [{"index": 1, "title": "专利法", "article": "第四十一条", "effective_date": "2021-06-01", "snippet": "…", "score": 0.83}]} {"type": "token", "delta": "根据《专利法》第四十一条"} {"type": "done", "latency_ms": {"first_token": 840, "total": 5300}, "answer_chars": 412} ``` 异常:`{"type":"error","code":"LLM_TIMEOUT|RETRIEVAL_FAILED|INTERNAL","message":"..."}`,前端据此显示重试按钮。 心跳:服务端每 20s 发 `{"type":"ping"}`,客户端回 `{"type":"pong"}`;连续 2 次无响应即关闭连接。 **约定**:`meta` 必须先到(前端据此渲染意图标签和"引用中"占位);`citations` 必须在第一个 `token` 之前到(前端要边流式边把 `〔来源 n〕` 渲染成可点击角标)。 --- ## 10. 红线 1. **拒答 / 兜底 / 异常提示不进缓存、不进历史。** 2. **多轮对话不读按 query 的答案缓存。** 3. **引用编号必须可追溯**:答案里出现的 `〔来源 n〕` 必须能在本轮 citations 里找到 n;找不到一律剔除,不得"补一个差不多的"。 4. **敏感内容不得原文外发**:用户提交的技术交底书、商标图样、客户名单必须先脱敏再送 DeepSeek;图片附件只做本地 OCR 或摘要,不整张上传。 5. **不输出"法律意见"式结论**:给依据 + 适用条件 + 建议咨询代理师/律师;不得承诺"一定能授权/一定能胜诉"。 6. **法条必须带版本与生效日期**,不得混用修正前后的条文。 7. **不阻塞事件循环**:任何同步 IO(MySQL、Milvus、模型)在 WebSocket 路径上都要走线程池。 8. **流式必须 flush**:日志用 `logging`,库代码禁止 `print`(CLI 入口除外)。 --- ## 11. 已知的坑(改代码时不要重新踩) | # | 现象 | 原因 | 规则 | |---|---|---|---| | 1 | `FileNotFoundError: knowledge/docs/...` | 相对路径以**进程 CWD** 为基准,uvicorn 的 CWD 未必是仓库根 | 程序资源一律 `BASE_DIR / ...`;只有用户传入的路径才按 CWD 解析 | | 2 | `ModuleNotFoundError: ip_system.xxx` / `No module named 'base'` | 用错了工作目录,或包布局与导入前缀不一致 | **本仓库统一为顶层包布局**:工作目录 = `ip_system/`,导入写 `from base.config import ...`(不带 `ip_system.` 前缀),启动用 `python main.py` / `python -m uvicorn app.server:app` / `python -m faq.bm25`。改了任一处的导入风格,其余全部要跟着改 | | 2b | `fastapi dev` 报 `No module named 'app'`,但 `uvicorn` 正常 | `ip_system/` 根目录存在 `__init__.py` 时,`fastapi-cli` 会一路向上拼模块名,把 `app/server.py` 解析成 `ip_system.app.server`,并把**上一级目录**塞进 `sys.path`,于是顶层导入 `from app.deps import ...` 失效 | **根目录不要放 `__init__.py`**(EduRAG 也没有)。`fastapi-cli` 的 [discover.py](E:\SoftWare\miniconda3\envs\edu_rag\Lib\site-packages\fastapi_cli\discover.py) 是按 `__init__.py` 逐级上溯的,根目录一旦成包,所有顶层导入都会错位 | | 3 | 策略永远是 Direct | 模型输出带前缀("策略:直接检索")、`max_tokens` 太小被截断、解析抛异常被吞 | 只给 8~16 token 预算 + 白名单映射 + 正则提取 + 默认 DIRECT,**日志里打印原始输出** | | 4 | `INVALID_PROMPT_INPUT: missing variables {'query'}` | 模板变量与调用点不一致 | 变量契约固定 `{context}` `{query}` `{history}`;改模板必须同步 `generate` 与 `stream` 两处 invoke | | 5 | 召回不少,进提示词只有 2 条 | `hybrid_search_with_rerank` 的 `top_n` 默认等于 `CANDIDATE_M`,内部先截一次、外层又截一次 | 需要更多候选时显式传 `top_n=TOP_K`,外层再截 | | 6 | 首字 3s+,前端像挂了 | 首次加载 BGE-M3(4.4G) + Reranker(3.2G) + BERT 意图模型;DeepSeek 云端首 token 还叠加网络 RTT | FastAPI `lifespan` 里做预热(空跑一次 encode + 一次 BERT 前向);前端保留"模型加载中"提示;埋点首字耗时 | | 7 | 流式"卡住"、并发请求互相等待 | 同步生成器直接跑在协程里,阻塞事件循环 | 用 `run_in_threadpool` / async client;日志用 `logging` 不用 `print` | | 8 | DeepSeek 调用超时 / 429 限流,答案只出一半 | 云端配额、并发或网络抖动,且没做重试 | `DEEPSEEK_TIMEOUT=60`;失败重试 1 次后走兜底话术;只有 `done` 才写历史,半截答案一律丢弃 | | 9 | 五类意图整体串位 / 总判成同一类 | BERT 的 `label2id` 与 `INTENT_LABELS` 顺序不一致,或训练与推理的 tokenizer / 截断长度不一致 | 训练时把 `label2id.json` 落盘,推理只读这份;上线前跑 golden set 看混淆矩阵;改标签顺序必须重训 | | 10 | 检索结果和预期差很远、向量维报错 | collection 的字段与维度在**创建时固定**,换模型/加字段不会自动迁移 | 改 schema 或换 embedding 模型 → 先 `drop_collection` 再重建再重跑 `ingest.py`;别指望 `upsert` 能改结构 | | 11 | BM25 召不回专业问题 | jieba 默认词典没有"权利要求书""创造性""显著性""PCT" | 维护 `faq/resources/ip_dict.txt`,启动时 `jieba.load_userdict`;阈值按新语料重新标定(8.0 是早期语料的经验值) | | 12 | 前端引用卡片错位 / 对不上号 | citations 数组顺序与提示词里的编号不一致;或模型自造编号 | 编号由后端在拼上下文时生成,citations 按同一顺序返回;生成后做越界剔除 | | 13 | 内网 nginx 后面看不到逐字效果,或 60s 后 504 | 反向代理默认缓冲响应、默认短超时 | `proxy_buffering off; proxy_read_timeout 300s;`,响应头带 `X-Accel-Buffering: no`;加心跳 | | 14 | 多轮里"那权利要求呢"答非所问 | 短句被分类成通用、检索 query 缺上文 | 分类与检索前先做查询改写(带上最近 1 轮),改写失败用原文;这条正是"短句不检索"问题的复现 | | 15 | 客户端断开后日志报一堆异常、历史里出现半截答案 | 生成任务没被取消,`finally` 里照旧写库 | 捕获 `WebSocketDisconnect` → 取消 → 半截答案丢弃;只有 `done` 才落库 | | 16 | 改完 FAQ 语料,答案还是旧的 | Redis L1 与语料快照没过期,L2 也没重灌 | 改语料 → `clear_cache()` → 重灌 MySQL → 重建 BM25 快照;把这三步写成一个脚本 | --- ## 12. 代码风格 - 注释、docstring、日志文案用中文;字符串统一单引号。 - 每个模块顶部:`logger = logging.getLogger(f'{BASE_DIR.name}.{__name__}')`。 - 公开函数写 type hints(`list[Document]`、`str | None`、`Iterator[str]`)。 - 库代码不 `print`;异常 `logger.error(..., exc_info=True)` 后**要么降级要么抛出**,不要吞掉。 - 提示词只改 `pipeline/prompts.py`,业务代码里不许出现长字符串模板。 - 新增依赖同步 `requirements.txt`(本子系统自包含,不依赖外部清单)。 - 单文件改动保持最小 diff:不重排 import、不顺手格式化整个文件。 --- ## 13. 改完必做的验证 > **工作目录 = `ip_system/` 目录本身**(`base/`、`pipeline/`、`app/` 是顶层包, > 代码里一律 `from base.config import ...`,不带 `ip_system.` 前缀;**本目录不要放 `__init__.py`**)。 ```bash # ① 语法 python -m py_compile main.py app/server.py pipeline/generator.py # ② FAQ 三级缓存冒烟(不需要 GPU、不需要网络,几秒返回) python -m faq.bm25 python main.py # 输入:专利被驳回怎么复审? → 来源应为「FAQ 标准答案」 # ③ 意图分类冒烟(本地 BERT,不需要 GPU) python -m pipeline.intent '商标被驳回可以复审吗' python -m pipeline.intent '你好' # 应走问候,不进分类 python tests/eval_intent.py # golden set 准确率 / 混淆矩阵 # 生成链路连通性(需要 .env 配好 DEEPSEEK_API_KEY) curl https://api.deepseek.com/v1/models -H "Authorization: Bearer $DEEPSEEK_API_KEY" # ④ 检索链路冒烟(会加载 BGE-M3 + Reranker) python -m store.milvus # 看召回文档与 rerank 分数 # ⑤ 端到端(CLI) python main.py # ⑥ WebSocket(服务先起) python -m uvicorn app.server:app --host 0.0.0.0 --port 8080 # 或者用 fastapi-cli(自带热重载):fastapi dev app/server.py --port 8080 python tests/ws_smoke.py # 校验 meta → citations → token* → done 顺序 # ⑦ 看日志 Get-Content logs/ip.log -Tail 80 # Windows tail -f logs/ip.log # Linux/macOS # 关键字:意图= / 缓存层= / 策略= / 引用= / 首字= ``` **回归顺序**:②(便宜)→ ③ → ⑥ → ⑤(贵)。FAQ 链路不依赖 GPU 和网络,永远是最快的回归入口。 --- ## 14. 里程碑(可作为任务来源) | 阶段 | 交付 | 验收标准 | |---|---|---| | M0 骨架 | 目录、`base/`、`requirements.txt`、日志 | `python -m py_compile` 全绿,`python main.py` 可启动并打印横幅 | | M1 FAQ 三级缓存 | `faq/` + `store/mysql.py` + `pipeline/cache.py` | 20 条 FAQ 问答全部命中,来源标注正确,第二次查询走 Redis(日志可见缓存层) | | M2 意图 | `pipeline/intent.py` + `intent_train.py` + `models/intent_bert/` | 每类 ≥ 200 条训练样本;五类 golden set 每类 20 条,准确率 ≥ 90%;低置信度样本能回落到关键词兜底;"你好"不进分类 | | M3 检索 | `store/milvus.py` + `strategies.py` + `router.py` | recall@5 ≥ 0.85;四策略可强制切换且各有日志 | | M4 生成与引用 | `generator.py` + `prompts.py` | 答案 `〔来源 n〕` 与 citations 100% 对齐;无越界编号;附免责声明 | | M5 接入层 | `app/server.py` + `web/` | WebSocket 事件顺序正确;首字 P95 < 3s;断开连接不写脏数据 | | M6 调优 | 评测脚本、阈值、词典 | 拒答率、误答率有基线;`FAQ_MIN_SCORE` / `TOP_K` / `CANDIDATE_M` 的取值有评测依据 | 评测集放 `tests/golden_set.jsonl`,每行至少:`query`、`expect_intent`、`expect_doc`(可空)、`expect_answerable`(bool,用来量拒答率)。 --- ## 15. 不要做的事 - 不要 import 本目录之外的代码(保持 `ip_system` 可整体搬迁为独立仓库)。 - 不要新建第二个 LLM 客户端封装:全项目统一走 `pipeline/generator.py` 里的一个 `ChatOpenAI` 实例(指向 DeepSeek flash),生成 / 改写 / 策略路由共用。 - 不要在线上调用 DeepSeek 做意图分类,也不要再引入第二套分类模型:意图只走本地 BERT + 关键词兜底。 - 不要在业务代码里拼提示词,不要在 `prompts.py` 之外维护第二份模板。 - 不要在 WebSocket 路径上做同步阻塞调用。 - 不要把拒答、兜底、异常话术写进缓存或历史。 - 不要为了"先跑通"注释掉降级逻辑、异常处理、引用校验。 - 不要修改 Milvus collection 结构而不重建(见坑 10)。 - 不要把 `.env`、`logs/`、`knowledge/docs/` 里的大文件、模型权重提交到 git。 - 不要给用户"保证授权 / 保证胜诉"式的结论,也不要省略免责声明。