代码语言

知识点思维导图

42 个知识节点

项目实战(04) - 项目:从最小 RAG 到可上线知识库

读完后,你应能完成以下任务:

  • 绘制“项目实战(04) - 项目:从最小 RAG 到可上线知识库 / 为什么先做这个项目”的关键对象与数据流,解释“面试高频:企业落地最多的就是知识库问答,面试官几乎一定会问 RAG。”,并用源码位置、日志或 Trace 标注证据。
  • 为“项目实战(04) - 项目:从最小 RAG 到可上线知识库 / 完整闭环长什么样”设计正常与异常输入,验证“这个项目的灵魂是最后那条分支:检索不到资料就老实说不知道。”,输出首个偏差位置与回归测试结果。
  • 实现“项目实战(04) - 项目:从最小 RAG 到可上线知识库 / 可运行 Demo:先跑通最小闭环”的最小代码或配置,检验“把下面代码保存为 main.py,执行 python main.py。”,输出命令、结果与 Diff,并说明不适用边界。

一、与进阶篇的分工

本篇保留为企业知识库 RAG 的基础项目:适合做第一个可演示闭环。进阶项目请读 91《企业级知识库项目》,那里会升级到多模态解析、对象存储、混合检索、权限过滤、引用回跳和量化评测。

二、为什么先做这个项目

如果你只能做一个 AI 项目放简历,选它。原因很实在:

  • 闭环清晰:上传文档 → 检索 → 回答 → 引用来源,每一步都看得见、讲得清。
  • 面试高频:企业落地最多的就是知识库问答,面试官几乎一定会问 RAG。
  • 能体现全栈:前端(聊天 UI、引用展示)、后端(接口、检索)、RAG(切分、检索、grounding)、工程化(日志、评测)全覆盖。
  • 可信度好讲:引用来源 + 资料不足时拒答,是体现工程深度的天然抓手。

三、完整闭环长什么样

flowchart TD
    D[文档] --> P[解析与 Chunk]
    P --> E[Embedding 与索引]
    Q[用户问题与 ACL] --> R[Top K 检索]
    E --> R
    R --> H{有可信证据?}
    H -->|是| G[基于证据生成]
    H -->|否| N[拒答或追问]
    G --> C[引用来源]
离线入库(一次):
  文档 → 解析文本 → 切分 chunk → 生成 embedding → 存向量库(带元数据)

在线问答(每次提问):
  用户问题 → 检索 topK chunk → 拼进 prompt → 模型基于资料回答 → 返回引用来源
              ↓ 检索为空
            明确拒答,不编造

这个项目的灵魂是最后那条分支:检索不到资料就老实说不知道。一个会编造的知识库,企业不敢用。

四、可运行 Demo:先跑通最小闭环

# requirements.txt
# 示例仅使用 Python 3.10+ 标准库,无第三方依赖。

把下面代码保存为 main.py,执行 python main.py。这里用词元重合模拟检索,只验证“召回、引用、拒答”契约;生产环境再替换成真实 Embedding、BM25 与模型生成。

跑完应看到两条带来源证据和一条资料不足拒答。把这三条路径讲清楚,项目的核心闭环才成立。

五、MVP 功能拆解(按这个顺序做)

模块 先做(MVP) 再做(进阶)
文档管理 上传 Markdown/TXT,查看解析状态 PDF/Word、批量导入
切分 按标题/段落切 chunk 表格切分、chunk 预览、overlap
检索问答 topK 检索 + 基于资料回答 混合检索、rerank、query 改写
引用来源 展示文件名 + 片段 点击定位原文
评测 维护 20 条问题集 命中率、正确率、坏 case 标签
工程化 日志、错误态、测试 权限隔离、限流、成本统计

先把这个最小闭环接上前端聊天框并保存 Trace,再逐项增加解析器、真实检索、流式输出和评测;每增加一层都保留可独立验收的输入与输出。

六、企业级版本怎么升级

MVP 跑通后,企业级版本重点补 5 块:

升级点 为什么要做
多格式解析 PDF、Word、Excel、图片、表格都要进知识库
父子分块 小块负责召回,大块负责回答上下文
混合检索 编号/术语靠 BM25,口语问法靠向量
权限过滤 检索阶段按部门、角色、密级过滤
原文定位 引用能回跳到文件、页码、表格或图片

如果要继续做进阶版,可阅读“工程基础”模块的《进阶:企业级 RAG 项目拆解》。该文会把文档解析、图文表资产、权限、评测和上线指标串成完整项目。

七、接口设计参考

POST /api/knowledge/upload      上传文件
GET  /api/knowledge/documents   文档列表
POST /api/rag/chat              知识库问答
GET  /api/rag/logs/:requestId   查看单次检索和生成日志

八、验收标准(也是演示脚本)

演示时按这个顺序走,最有说服力:

  1. 上传一份制度文档 → 展示解析状态
  2. 问一个能命中的问题("报销几天内提交")→ 回答 + 引用来源
  3. 问一个知识库没有的问题("年假几天")→ 明确说资料不足
  4. 打开日志 → 展示这次检索命中了哪些 chunk、得分多少

第 3 步是亮点:主动展示"我不会瞎编",比第 2 步答对更能打动面试官。

九、工程上真正会踩的坑

  • chunk 切太大:一段塞几百字,检索命中了但答案被淹没。按句/小段切,配合 overlap。
  • 检索为空还硬答:模型会拿不相关资料编一个答案。if not hits: 拒答 是硬性兜底。
  • 引用来源对不上正文:回答用了 A 资料却标 B 来源。citation 必须从实际命中的 chunk 元数据生成,不能事后补。
  • 没有评测集:改了切分参数不知道是变好还是变坏。哪怕只有 20 条问题,也要能跑个命中率。

十、简历怎么写

独立实现企业知识库 RAG 助手,支持文档入库、chunk 切分、语义检索、引用来源、流式问答和坏 case 评测。前端展示检索来源、生成状态和错误重试;后端记录 requestId、检索命中、模型耗时和 token 成本。引入资料不足拒答机制,将编造率控制在可接受范围。

如果要写成企业级版本,可以这样升级:

设计企业级知识库 RAG 系统,支持多格式文档解析、父子分块、BM25+向量混合检索、rerank 精排、权限过滤和原文定位;回答引用可回跳到文件页码、表格或图片证据。维护包含正负样本的评测集,跟踪 Hit Rate@K、拒答准确率和坏 case 回归,保证知识库不是只跑通 demo,而是可持续调优。

十一、动手实践:43 企业知识库 RAG 项目

一个本地可运行的真实企业 RAG 工作台:文档入库、Markdown 批量导入、chunk 切分、本地 embedding、Qdrant 向量数据库检索、基于资料回答、引用来源和检索 Trace。

当前版本不依赖 OpenAI Key。embedding 使用本地 hashing 向量模型,向量存储使用 Docker 中的 Qdrant,适合先讲清企业 RAG 工程闭环;后续可把 LocalEmbeddingModel 换成 OpenAI / bge / m3e 等真实 embedding 模型。

11.1 环境

  • Python 3.10+
  • Docker Desktop
  • Qdrant:通过 docker compose 启动,无需手动安装

11.2 启动

cd /Users/imber/Desktop/ai-lab/worktrees/knowledge-categories/imber-blog/src/content/knowledge/03-AI大模型应用开发/02-企业级知识库/04-企业知识库项目/01-项目-企业知识库RAG/lab
docker compose up -d qdrant
python3 main.py

浏览器打开:

http://127.0.0.1:8043

Qdrant REST:

http://127.0.0.1:6333

11.3 导入 agent 小册

页面点击 导入 AI 应用手册,会从当前仓库位置自动解析并读取完整的 AI 大模型应用开发目录:

03-AI大模型应用开发

导入规则兼容新版目录结构:

  • **/*.md
  • **/09-附录/*.md
  • 排除当前项目的 lab/

导入后会重建 Qdrant collection:

agent_manual_chunks

本次验证导入结果:

导入数量会根据当前 `knowledge/Agent` 目录中的文章实时变化。

11.4 测试

python3 -m unittest discover -s tests -v
python3 -m py_compile main.py rag_core.py
node --check static/app.js

11.5 演示问题

导入 agent小册 后可以问:

语义相近的文本为什么向量也相近?
引用来源为什么不能让模型自己报?
RAG 回答为什么要引用来源?
检索与重排 Rerank 有什么区别?

验证结果示例:

  • 语义相近的文本为什么向量也相近? 命中 22-Embedding向量化.md
  • 引用来源为什么不能让模型自己报? 命中 25-RAG回答生成与引用来源.md

11.6 接口

方法 路径 说明
GET /health 服务健康检查,返回文档数、chunk 数、Qdrant 状态
GET /api/knowledge/documents 文档列表
POST /api/knowledge/documents 新增文档,JSON 字段:title/source/text
POST /api/knowledge/import-agent-manual 批量导入本地 AI 应用知识集(路径保留兼容)
POST /api/rag/chat 知识库问答,JSON 字段:question/topK
GET /api/rag/logs 最近问答日志
GET /api/rag/logs/:requestId 单次问答日志

11.7 代码结构

文件 作用
docker-compose.yml 启动 Qdrant 向量数据库
rag_core.py RAG 核心:Markdown 清洗、切分、本地 embedding、Qdrant client、检索、拒答、引用、日志
main.py 标准库 HTTP 服务、API 路由、静态页面服务、agent 小册导入
static/ 前端工作台
data/sample-docs/ 首次启动时的样例企业文档
tests/ 核心逻辑、API、向量导入测试

11.8 后续升级

  • LocalEmbeddingModel 换成真实 embedding 模型。
  • 给 Markdown 解析增加标题层级、chunk overlap 和 chunk 预览。
  • 加评测集,记录命中率、拒答率和坏 case。
  • 接入真实 LLM,让 _compose_answer 从“拼接资料”升级为“基于资料生成自然语言回答”。

十二、总结

  • 与进阶篇的分工:本篇保留为企业知识库 RAG 的基础项目:适合做第一个可演示闭环。
  • 为什么先做这个项目:面试高频:企业落地最多的就是知识库问答,面试官几乎一定会问 RAG。
  • 完整闭环长什么样:这个项目的灵魂是最后那条分支:检索不到资料就老实说不知道。
  • 可运行 Demo:先跑通最小闭环:这里用词元重合模拟检索,只验证“召回、引用、拒答”契约;
  • MVP 功能拆解(按这个顺序做):先把这个最小闭环接上前端聊天框并保存 Trace,再逐项增加解析器、真实检索、流式输出和评测;
  • 企业级版本怎么升级:| 父子分块 | 小块负责召回,大块负责回答上下文 |

12.1 实现源码与运行边界

data/sample-docs/finance-handbook.md

# 财务报销手册

报销需在费用产生后 30 天内提交,逾期需补充直属主管说明。

差旅报销包含交通、住宿、餐补三类,餐补每天上限 80 元。

单笔金额超过 5000 元的采购报销,需要部门负责人和财务负责人双重审批。

发票抬头必须使用公司全称,电子发票需上传原始 PDF 文件。

data/sample-docs/hr-policy.md

# 人事制度

请假需提前 1 天在 OA 系统提交申请,由直属主管审批。

病假需在 3 个工作日内补交医院证明,无法补交时按事假处理。

试用期员工转正评估在入职满 80 天后发起,由直属主管填写评价。

员工离职需至少提前 30 天提交申请,并完成工作交接清单。

data/sample-docs/it-support.md

# IT 支持手册

VPN 无法连接时,请先确认员工账号未过期,并重启客户端。

企业邮箱首次登录需要绑定手机验证码,验证码 5 分钟内有效。

电脑遗失或怀疑账号泄露时,必须在 1 小时内联系 IT 值班同学冻结账号。

共享文档权限默认按部门开放,跨部门共享需由文档负责人确认。

docker-compose.yml

services:
  qdrant:
    image: qdrant/qdrant:v1.14.1
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_data:/qdrant/storage
    restart: unless-stopped

volumes:
  qdrant_data:

docs/IMPLEMENTATION_PLAN.md

# 43 企业知识库 RAG 实施计划

**目标:** 把原来的单文件命令行 demo 升级为可演示的本地 Web 项目,支持文档入库、RAG 问答、引用来源、检索日志和友好的前端界面。

**方案:** 使用 Python 标准库提供 HTTP 服务与静态文件服务,核心 RAG 逻辑拆到 `rag_core.py`。检索用本地 TF-IDF + 余弦相似度实现,避免外部 API Key 和向量库依赖。

**文件结构:**

- `rag_core.py`:文档模型、chunk 切分、TF-IDF 检索、grounded 回答、请求日志。
- `main.py`:HTTP API、静态页面服务、示例数据加载、启动入口。
- `static/index.html`、`static/styles.css`、`static/app.js`:企业知识库工作台前端。
- `data/sample-docs/*.md`:开箱即用的企业制度样例。
- `tests/test_rag_core.py`、`tests/test_api.py`:核心逻辑和 API 测试。
- `README.md`:运行、测试、演示脚本和升级方向。

**验收:**

1. `python3 -m unittest discover -s tests -v` 通过。
2. `python3 main.py` 能启动服务。
3. `GET /health` 返回 ok。
4. `POST /api/rag/chat` 对“报销多久内提交”返回答案、引用和 trace。
5. 前端页面能加载文档列表、提问、展示引用和检索过程。

main.py

rag_core.py

static/app.js

const statusOutput = document.querySelector('#status'); // 健康与导入状态区域。
const answerOutput = document.querySelector('#answer'); // 回答、引用和 Trace 区域。

async function request(path, options = {}) {
  const response = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...options }); // API 原始响应。
  const payload = await response.json(); // JSON 响应对象。
  if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  return payload;
}

document.querySelector('#health').addEventListener('click', async () => {
  try { statusOutput.textContent = JSON.stringify(await request('/health'), null, 2); }
  catch (error) { statusOutput.textContent = error.message; }
});

document.querySelector('#import').addEventListener('click', async () => {
  statusOutput.textContent = '正在重建 collection 并导入...';
  try { statusOutput.textContent = JSON.stringify(await request('/api/knowledge/import-agent-manual', { method: 'POST', body: '{}' }), null, 2); }
  catch (error) { statusOutput.textContent = error.message; }
});

document.querySelector('#add').addEventListener('click', async () => {
  const payload = { title: document.querySelector('#title').value, source: document.querySelector('#source').value, text: document.querySelector('#text').value }; // 手工文档对象。
  try { statusOutput.textContent = JSON.stringify(await request('/api/knowledge/documents', { method: 'POST', body: JSON.stringify(payload) }), null, 2); }
  catch (error) { statusOutput.textContent = error.message; }
});

document.querySelector('#ask').addEventListener('click', async () => {
  const question = document.querySelector('#question').value.trim(); // 清洗后的问答查询。
  answerOutput.textContent = '正在向量检索...';
  try { answerOutput.textContent = JSON.stringify(await request('/api/rag/chat', { method: 'POST', body: JSON.stringify({ question, topK: 4 }) }), null, 2); }
  catch (error) { answerOutput.textContent = error.message; }
});

static/index.html

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>企业知识库 RAG 工作台</title>
  <style>
    body { margin: 0; font: 15px/1.6 system-ui; color: #171717; background: #f4f5f7; }
    main { max-width: 980px; margin: 32px auto; padding: 0 20px; }
    section { padding: 20px; background: white; border: 1px solid #ddd; margin-bottom: 16px; }
    textarea, input { box-sizing: border-box; width: 100%; padding: 10px; margin: 6px 0; }
    button { padding: 9px 14px; margin-right: 8px; }
    pre { white-space: pre-wrap; background: #111; color: #d9fbe9; padding: 14px; overflow: auto; }
  </style>
</head>
<body>
  <main>
    <h1>企业知识库 RAG 工作台</h1>
    <section><button id="health">检查健康</button><button id="import">导入 AI 应用手册</button><pre id="status">尚未检查</pre></section>
    <section><h2>新增文档</h2><input id="title" placeholder="标题"><input id="source" placeholder="来源"><textarea id="text" rows="5" placeholder="# 制度标题&#10;## 章节&#10;正文"></textarea><button id="add">切分并入库</button></section>
    <section><h2>在线问答</h2><input id="question" value="RAG 回答为什么要引用来源?"><button id="ask">检索并回答</button><pre id="answer">等待提问</pre></section>
  </main>
  <script src="/app.js"></script>
</body>
</html>

tests/test_rag_core.py

12.2 可运行实验:RAG ACL 与跨租户泄漏

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>AA-06 在线实验</title>
  <style>
    :root{color-scheme:dark;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:#0f1211;color:#e7ece9;font-size:13px}.shell{padding:16px}.top{display:flex;justify-content:space-between;gap:16px;margin-bottom:14px}h1{margin:3px 0;font-size:18px}.id,.value{color:#68e0b5;font-family:ui-monospace,monospace}.summary{margin:4px 0;color:#a5afa9}.run{border:0;border-radius:6px;background:#68e0b5;color:#07110d;padding:8px 14px;font-weight:700}.grid{display:grid;grid-template-columns:minmax(220px,.8fr) minmax(0,1.8fr);gap:12px}.panel{border:1px solid #29322e;background:#141817;padding:12px}.control{display:grid;gap:5px;margin-bottom:11px}.head{display:flex;justify-content:space-between;gap:8px}select,input{width:100%;accent-color:#68e0b5;background:#0d100f;color:#e7ece9}.toggle{display:flex;justify-content:space-between;border-top:1px solid #29322e;padding-top:9px}.toggle input{width:18px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.metric{border:1px solid #29322e;padding:8px}.metric b{display:block;color:#68e0b5;font-size:16px}.stages{display:flex;gap:6px;overflow:auto;margin:10px 0}.stage{border:1px solid #8a6230;padding:7px;min-width:90px}.stage.ok{border-color:#367a61}.stage.fail{border-color:#8b4545}table{width:100%;border-collapse:collapse}td{border-top:1px solid #29322e;padding:7px}.diagnosis{margin-top:9px;border-left:3px solid #68e0b5;background:#101412;padding:9px;line-height:1.5}.danger{border-color:#ef7f7f}@media(max-width:680px){.top,.grid{display:grid;grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}}
  </style>
</head>
<body>
  <main class="shell">
    <header class="top"><div><div class="id">AA-06 · DETERMINISTIC LAB</div><h1 id="title"></h1><p class="summary" id="summary"></p></div><button class="run" id="run">运行实验</button></header>
    <section class="grid"><div class="panel"><div id="controls"></div><label class="toggle"><span>注入典型故障</span><input id="failure" type="checkbox"></label></div><div class="panel"><div class="metrics" id="metrics"></div><div class="stages" id="stages"></div><table><tbody id="rows"></tbody></table><div class="diagnosis" id="diagnosis"></div></div></section>
  </main>
  <script>
    const scenario = { title: 'RAG ACL 与跨租户泄漏', summary: '切换鉴权时机、缓存键和租户,验证候选与引用是否会越权。', controls: [
    { key: 'filterStage', label: 'ACL 执行时机', type: 'select', value: 'before', options: [['none', '无 ACL'], ['after', '召回后'], ['before', '召回前']] },
    { key: 'cacheKey', label: '缓存键组成', type: 'select', value: 'full', options: [['query', '仅 Query'], ['tenant', 'Query + Tenant'], ['full', 'Query + Tenant + ACL 摘要']] },
    { key: 'role', label: '当前角色', type: 'select', value: 'employee', options: [['guest', '访客'], ['employee', '员工'], ['finance', '财务管理员']] }
  ] };
    const controls = document.querySelector('#controls');
    const failure = document.querySelector('#failure');
    document.querySelector('#title').textContent = scenario.title;
    document.querySelector('#summary').textContent = scenario.summary;
    function renderControl(control) {
      const label = document.createElement('label'); label.className = 'control';
      const head = document.createElement('span'); head.className = 'head'; head.innerHTML = '<span>' + control.label + '</span><span class="value" data-value="' + control.key + '"></span>'; label.appendChild(head);
      const input = document.createElement(control.type === 'select' ? 'select' : 'input'); input.dataset.key = control.key;
      if (control.type === 'select') control.options.forEach(option => { const item = document.createElement('option'); item.value = option[0]; item.textContent = option[1]; item.selected = option[0] === control.value; input.appendChild(item); });
      else { input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step || 1; input.value = control.value; }
      input.addEventListener('input', updateValues); label.appendChild(input); return label;
    }
    function updateValues() { scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); document.querySelector('[data-value="' + control.key + '"]').textContent = control.type === 'select' ? input.options[input.selectedIndex].text : input.value + (control.suffix || ''); }); }
    function readValues() { const values = {}; scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); values[control.key] = control.type === 'range' ? Number(input.value) : input.value; }); values.failure = failure.checked; return values; }
    function stage(name, state, detail) { return { name, state, detail }; }
    const aiStage = stage;
    function clamp(value, minimum, maximum) { return Math.min(maximum, Math.max(minimum, value)); }
    function simulate(values) { const fail = values.failure;
      /** 当前角色可访问的样例文档数量。 */
      const visible = values.role === 'finance' ? 12 : values.role === 'employee' ? 8 : 3;
      /** ACL 和缓存键不完整造成的泄漏数。 */
      const leaks = values.filterStage !== 'before' || values.cacheKey !== 'full' || fail ? (values.role === 'guest' ? 5 : 2) : 0;
      return { metrics: [[visible, '合法文档'], [leaks, '泄漏候选'], [values.filterStage.toUpperCase(), 'ACL 时机'], [leaks ? 'DENY' : 'ALLOW', '回答决策']], stages: [aiStage('解析身份', 'ok', values.role), aiStage('生成 ACL 摘要', fail ? 'fail' : 'ok', 'tenant + groups'), aiStage('召回过滤', values.filterStage === 'before' ? 'ok' : 'fail', values.filterStage), aiStage('缓存键', values.cacheKey === 'full' ? 'ok' : 'fail', values.cacheKey), aiStage('引用鉴权', leaks ? 'fail' : 'ok', leaks ? 'blocked' : 'pass')], rows: [['跨租户', values.filterStage === 'before' ? '向量与关键词查询均带 tenant_id' : '候选先进入内存,存在日志与缓存泄漏'], ['缓存隔离', values.cacheKey === 'full' ? '包含 tenant、权限摘要和知识库版本' : '不同权限用户可能复用同一答案'], ['引用接口', leaks ? '二次鉴权阻断返回,记录安全事件' : '下载或预览前再次校验文档权限']], diagnosis: leaks ? '检测到跨权限候选。系统必须拒答并修复召回、缓存和引用三层边界。' : '权限在检索前、缓存键和引用接口三处闭环。', danger: leaks > 0 };
     }
    function render() { const result = simulate(readValues()); document.querySelector('#metrics').innerHTML = result.metrics.map(item => '<div class="metric"><b>' + item[0] + '</b><span>' + item[1] + '</span></div>').join(''); document.querySelector('#stages').innerHTML = result.stages.map(item => '<div class="stage ' + item.state + '"><b>' + item.name + '</b><div>' + item.detail + '</div></div>').join(''); document.querySelector('#rows').innerHTML = result.rows.map(item => '<tr><td>' + item[0] + '</td><td>' + item[1] + '</td></tr>').join(''); const diagnosis = document.querySelector('#diagnosis'); diagnosis.textContent = result.diagnosis; diagnosis.className = 'diagnosis' + (result.danger ? ' danger' : ''); }
    scenario.controls.forEach(control => controls.appendChild(renderControl(control))); updateValues(); document.querySelector('#run').addEventListener('click', render); render();
  </script>
</body>
</html>

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“项目:从最小 RAG 到可上线知识库”中,需要同时满足“与进阶篇的分工”与“为什么先做这个项目”。给定正文约束“进阶项目请读 91《企业级知识库项目》,那里会升级到多模态解析、对象存储、混合检索、权限过滤、引用回跳和量化评测。”,哪些判断保持了原有处理机制?多选
2“项目:从最小 RAG 到可上线知识库”出现偏差:“在“项目:从最小 RAG 到可上线知识库 / 完整闭环长什么样”中,即使不满足“检索不到资料就老实说不知道”,结果与副作用仍会保持不变。”已成为实际行为。围绕“完整闭环长什么样”与“可运行 Demo:先跑通最小闭环”,哪些判断能定位被改变的职责或边界?多选
3评审“项目:从最小 RAG 到可上线知识库”方案时,验收条件包含“先把这个最小闭环接上前端聊天框并保存 Trace,再逐项增加解析器、真实检索、流式输出和评测;”。关于“MVP 功能拆解(按这个顺序做)”与“企业级版本怎么升级”的哪些决策符合正文机制?多选