【FDE系列】阶段3:Day 65:向量数据库入门 — Chroma 与本周收官

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段3·Day 65:向量数据库入门 --- Chroma 与本周收官 🎉

FDE 学习系列教程 · 第三阶段 · 第 9 周 · Day 5(收官) 预计时长:3.5 小时 | 难度:★★★★☆ | 前置知识:Day 61-64(RAG 链路、BGE-M3、文档解析、文本分块) 对标大纲课时:3.2.5 向量数据库 ------ 选型对比:大规模用 Milvus,已有 PG 用 PGVector,快速原型用 Chroma,高性能用 Qdrant(本周收官)
📌 一句话目标:搞懂向量库在干什么,用 Chroma 把 Day63 解析、Day64 分块、Day62 向量化的手册灌成一个可查询的知识库,产出"知识库问答 v0.2"并完成本周复盘。


🧑‍🤝‍🧑 开场:四块零件,今天合体

回头看看这周你攒下的东西:

复制代码
  Day61  RAG 全景      八步链路图 + 最小 RAG 脚本(字符重合打分)
  Day62  Embedding     BGE-M3 把文字变成 1024 维向量,语义检索可用了
  Day63  文档解析      MinerU / Docling 把真实 PDF 变成干净 Markdown
  Day64  文本分块      四种策略对比,选出最适合手册的切法

  你手里已有:一份干净的 manual_a3.clean.md、一组切好的 chunk、
             一个 BGE-M3 模型、一套验证过的分块参数。
  缺的      :一个能把它们【存起来、查得快】的地方。

回看第二阶段:那时你要存数据,第一个想到的是 MySQL------表、行、字段、索引、SQL 查询。今天要存的是"向量",MySQL 不会算余弦相似度,所以需要新东西:向量数据库。

复制代码
  第二阶段:结构化数据   → MySQL(按字段精确匹配 + 范围查询)
  今天    :非结构化语义 → 向量库(按语义相似度模糊匹配)

  两者是搭档,不是替代:
    工单表 / 设备台账 → MySQL(第二阶段的家底)
    手册 / 维修记录   → 向量库(今天的增量)
    一个 chunk 用 id 就能关联回 MySQL 的设备记录

今天的产出是知识库问答 v0.2:第二阶段的设备告警工单闭环系统 + 第 7-8 周冻结的 v0.1(分类/提取/报告)+ 本周新增的手册问答。

复制代码
  第二阶段工单闭环(FastAPI+MySQL+Docker+飞书)
        ↓
  v0.1(第7-8周冻结):工单分类 / 信息提取 / 巡检报告
        ↓
  v0.2(今天)🆕:问"这台设备怎么修" → 检索手册 → 带引用回答

今天五件事,收官日内容多一点,节奏跟紧:① 向量库到底在干什么(为什么不能拿 numpy 硬算);② 四个选型怎么挑(Chroma / Qdrant / Milvus / PGVector);③ Chroma 上手:持久化、四件套概念、增删查;④ 完整项目:灌库脚本 + 查询脚本,跑通"问手册→带引用回答";⑤ 本周复盘 + 验收清单 + 第 10 周预告。


📖 一、向量库是干嘛的:存向量 + 快速找邻居

本质就两件事

复制代码
  向量数据库 = 存储 + 近似最近邻检索(ANN)

  ① 存:向量 + 原文 + 元数据(来源/页码/章节/密级)
  ② 查:给你一个向量,找出库里跟它最像的 K 个

  就这两件事,没有别的。

用 Day 62 的类比说:向量库就是"图书管理员"的索引卡片柜------你给它一个问题向量,它按卡片快速找出最相似的几本书(chunk),把书和你一起交给模型。

为什么不直接拿 numpy 硬算?

你可能会想:Day 62 我不就是用 DOC_VECS @ qv 一行算出相似度了吗?要什么数据库?

因为那个做法只在"5 条资料"时成立。 暴力算法要把查询向量和库里每一个向量都比一次:

资料量 每次查询的计算量 可行性
100 条 100 次内积 ✅ 瞬间
1 万条 1 万次 ✅ 毫秒级,还行
100 万条 100 万次 × 1024 维 ⚠️ 秒级,用户开始等
1 亿条 1 亿次 ❌ 不可能,内存也放不下

向量库的答案是 ANN(Approximate Nearest Neighbor,近似最近邻):

复制代码
  不追求"绝对最相似的 K 个",而是"99% 概率最相似的 K 个"
  用预先建好的索引结构跳过绝大多数不可能的候选

  主流索引:HNSW(分层可导航小世界图)------ Chroma/Qdrant/Milvus 都用
  直觉:给向量建"好友网络图",查询时从入口点出发每次跳到"更像目标"的邻居,
        几步就到 → 复杂度从 O(n) 降到 O(log n)

  代价:索引占内存,结果是"近似"的(可能漏掉第 K+1 名,概率极低)
  收益:100 万条数据,查询从秒级降到毫秒级

💡 FDE 的判断标准 :资料量 < 1 万条且离线批处理 → numpy 够用,别过度设计;要在线服务、增量更新、持久化、元数据过滤 → 上向量库。客户现场手册几十份、切完几万到几十万块,必须上。

一条记录长什么样

复制代码
  ┌─────────────────────────────────────────────────────────┐
  │  id         "manual_a3-0042"     ← 唯一标识(你自己定)     │
  │  embedding  [0.021, -0.113, ...] ← 1024 维向量(检索用)   │
  │  document   "§4.3 冷却系统维护..."  ← 原文(喂给模型用)      │
  │  metadata   {source, page:47, chapter, device:"A3", acl} │
  └─────────────────────────────────────────────────────────┘
  embedding → 算相似度   document → 拼进 Prompt
  metadata  → 过滤(只看 A3)+ 溯源(标 P47)

⚠️ metadata 是被严重低估的资产。第 10 周 Day 69 的权限过滤完全靠它实现("谁能看到哪些 chunk")。今天灌库就把 metadata 设计好,别等上线返工。


📖 二、选型对比:四个选手怎么挑

大纲 3.2.5 给了明确指引:"大规模用 Milvus,已有 PG 用 PGVector,快速原型用 Chroma,高性能用 Qdrant"。展开成一张表:

维度 Chroma Qdrant Milvus PGVector
Star 量级 ~29K ~34K ~46K PG 扩展
定位 原型 / 嵌入式 生产级通用 大规模分布式 PG 生态补充
部署难度 ⭐ 极简(pip install 即用) ⭐⭐ Docker 一条命令 ⭐⭐⭐⭐ 需 etcd/minio 等多组件 ⭐⭐ 给现有 PG 装扩展
运行形态 嵌入式(进程内)/ 单机服务 独立服务(gRPC+HTTP) 分布式集群 PostgreSQL 的一个扩展
数据规模 百万级以内舒适 千万级 亿级 千万级以内
元数据过滤 ✅ 基础 ✅✅ 强(payload index) ✅✅ 强 ✅✅ 最强(直接写 SQL)
混合检索 一般 ✅ 原生支持稀疏向量 ✅ 支持 需配合全文检索
语言客户端 Python/JS 为主 Python/Go/Rust/JS/Java 多语言 任何能连 PG 的语言
运维成本 极低 中 高 低(复用现有 PG 运维)
适合场景 学习/原型/小工具 生产主力 超大规模、多租户 已有 PostgreSQL

FDE 的决策树 :① 已有 PostgreSQL 且量 < 千万级 → PGVector(不引入新组件,审核最容易过);② 量 > 亿级 / 多租户 / 有专职运维 → Milvus;③ 要生产、要性能、过滤强、千万级 → Qdrant (第 10 周 Day 66 用它);④ 原型 / demo / 本地开发 / 百万级内 → Chroma(今天用它)。

💡 工程建议 :原型用 Chroma,上线换 Qdrant,两者代码差别不大(都是 add / query / 过滤)。今天学的 collection、metadata、top-k、距离到 Qdrant 完全平移,不用担心白学。

为什么今天从 Chroma 开始 :pip install chromadb 就能跑,不需要 Docker、不需要起服务;PersistentClient 一行搞定持久化;API 极简;get_or_create_collection 幂等,脚本可反复跑。⚠️ 唯一坑是默认嵌入模型 all-MiniLM-L6-v2 是英文模型,中文效果差------我们用"自己传 BGE-M3 向量"绕开它。


🖥️ 三、Chroma 上手:从安装到查询

实操步骤 1:安装

复制代码
cd fde-ai
.\.venv\Scripts\Activate.ps1

pip install chromadb

⚠️ 如果与 FlagEmbedding 的 torch/pydantic 版本冲突,按 Day 63 的经验单独建环境:

复制代码
python -m venv .venv-vdb
.\.venv-vdb\Scripts\Activate.ps1
pip install chromadb

然后把灌库脚本和查询脚本都放这个环境里跑(它们本来就是独立进程)。

实操步骤 2:Hello Chroma ------ 先跑通最小闭环

新建 day65_hello_chroma.py:

复制代码
"""Day65 第一步:Chroma 最小闭环 ------ 建库 / 灌数据 / 查询"""
import chromadb

# ① 持久化客户端:数据落盘到 ./chroma_db,重启不丢
client = chromadb.PersistentClient(path="./chroma_db")

# ② collection(集合):相当于 MySQL 里的"一张表"
#    get_or_create = 有就取、没有就建 → 脚本反复跑不报错(幂等)
col = client.get_or_create_collection(
    name="manual",
    metadata={"hnsw:space": "cosine"},     # 相似度空间:cosine / l2 / ip
)

# ③ add:灌入 3 条(embeddings 先手写假向量,下一步换成 BGE-M3)
col.add(
    ids=["c1", "c2", "c3"],
    documents=[
        "§4.3 冷却系统维护:冷却水进水压力正常范围 0.3~0.5MPa。",
        "§6.1 加热系统:加热圈单段电阻标准 18±2 欧姆。",
        "§7.4 液压系统:液压油每 500 小时更换一次。",
    ],
    embeddings=[[0.10, 0.20, 0.30], [0.90, 0.10, 0.05], [0.15, 0.85, 0.10]],
    metadatas=[
        {"source": "manual_a3.pdf", "page": 47, "chapter": "4.3 冷却系统维护", "device": "A3"},
        {"source": "manual_a3.pdf", "page": 62, "chapter": "6.1 加热系统", "device": "A3"},
        {"source": "manual_a3.pdf", "page": 88, "chapter": "7.4 液压系统", "device": "A3"},
    ],
)
print("灌入后条数:", col.count())

# ④ query:用一个查询向量找最像的 2 条
res = col.query(query_embeddings=[[0.12, 0.22, 0.31]], n_results=2)
for i in range(len(res["ids"][0])):
    md = res["metadatas"][0][i]
    print(f"  id={res['ids'][0][i]}  距离={res['distances'][0][i]:.4f}  来源=P{md['page']}")
    print(f"     {res['documents'][0][i][:50]}")

python day65_hello_chroma.py

输出:

复制代码
collection: manual 已有条数: 0
灌入后条数: 3

查询结果:
  id=c1  距离=0.0000  来源=P47
     §4.3 冷却系统维护:冷却水进水压力正常范围 0.3~0.5MPa。
  id=c3  距离=0.2537  来源=P88
     §7.4 液压系统:液压油每 500 小时更换一次。

三个必须建立的认知:

  1. 返回的是距离 不是相似度 → 0.0000 表示一模一样(最相关)。⚠️ 距离越小越像,跟相似度相反!

  2. metadatas 原样带回来了 → 你终于能说出"这个答案来自 P47"。

  3. ./chroma_db 目录已生成,关掉 Python 再进数据还在 → 持久化生效。

实操步骤 3:metadata 过滤(where)

这是向量库比 numpy 强的一大关键------先按条件圈定范围,再在范围内做语义检索。

复制代码
"""Day65:where 元数据过滤"""
import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
col = client.get_or_create_collection(name="manual",
                                      metadata={"hnsw:space": "cosine"})

# 灌两条别的设备的,制造对比
col.add(ids=["c4", "c5"],
        documents=["B2 冲压机冷却系统:冷却水压力 0.35~0.55MPa。",
                   "B2 冲压机液压系统:液压油每 400 小时更换一次。"],
        embeddings=[[0.13, 0.21, 0.29], [0.16, 0.84, 0.11]],
        metadatas=[{"source": "manual_b2.pdf", "page": 30, "chapter": "3.1 冷却系统", "device": "B2"},
                   {"source": "manual_b2.pdf", "page": 55, "chapter": "5.2 液压系统", "device": "B2"}])

QV = [0.12, 0.22, 0.31]     # 一个偏向"冷却系统"的查询向量


def show(title: str, **kw):
    r = col.query(query_embeddings=[QV], n_results=3, **kw)
    print(f"\n【{title}】")
    for i in range(len(r["ids"][0])):
        md = r["metadatas"][0][i]
        print(f"  {md['device']}  P{md['page']}  距离={r['distances'][0][i]:.4f}")


show("不过滤")
show("只看 A3", where={"device": "A3"})
show("页码 > 50", where={"page": {"$gt": 50}})
show("A3 且页码>40", where={"$and": [{"device": "A3"}, {"page": {"$gt": 40}}]})

支持的操作符:

操作符 含义 示例
$eq 等于(默认) {"device": "A3"}
$ne 不等于 {"device": {"$ne": "B2"}}
$gt / $gte 大于 / 大于等于 {"page": {"$gt": 50}}
$lt / $lte 小于 / 小于等于 {"page": {"$lte": 60}}
$in / $nin 在 / 不在列表内 {"device": {"$in": ["A3", "A5"]}}
$and / $or 且 / 或 {"$and": [{"device":"A3"}, {"page":{"$gt":40}}]}

📌 这个 where 就是第 10 周 Day 69「权限过滤」的技术基础 。今天先把 device、acl(可访问角色)这类字段灌进去,将来加权限就是加一个 where 条件的事。

实操步骤 4:改、删与运维操作

复制代码
"""Day65:Chroma 的改 / 删 / 运维操作"""
import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
col = client.get_or_create_collection(name="manual",
                                      metadata={"hnsw:space": "cosine"})

# 改:upsert(有则更新,无则插入)------ 手册改了一页就用它
col.upsert(ids=["c1"], documents=["§4.3 冷却系统维护:压力范围修订为 0.32~0.5MPa。"],
           embeddings=[[0.11, 0.20, 0.30]],
           metadatas=[{"source": "manual_a3.pdf", "page": 47,
                       "chapter": "4.3 冷却系统维护", "device": "A3"}])
print("upsert 后:", col.get(ids=["c1"])["documents"])

# 删:按 id 或按条件
col.delete(ids=["c5"])
# col.delete(where={"device": "B2"})        # 按条件删,慎用
print("删除后条数:", col.count())

# 运维:列集合 / 翻看内容 / 删集合
print("所有集合:", [c.name for c in client.list_collections()])
print("前 3 条 id:", col.peek(limit=3)["ids"])
# client.delete_collection("manual")        # 删整个集合(重建时用)

🖥️ 四、完整项目:把手册灌成可查询的知识库

现在把 Day 62/63/64 的成果全部合进来。这是本周的主线产出。

项目结构

复制代码
fde-ai/
├── .env / .gitignore             # Key 与 chroma_db/ 都要忽略
├── data/cleaned/manual_a3.clean.md    # Day63 清洗后 ⭐(今天的原料)
├── kb/                           # 今天的知识库模块
│   ├── embedder.py               # Day62:BGE-M3 封装(全项目唯一入口)
│   ├── chunker.py                # Day64:分块策略
│   ├── build_index.py            # ① 灌库脚本(离线跑,资料变了才重跑)
│   ├── retriever.py              # ② 检索封装(在线)
│   └── answer.py                 # ③ 问答:检索 + DeepSeek 生成 + 引用
├── day65_ask.py                  # 命令行入口
└── chroma_db/                    # 向量库落盘目录

📌 落库脚本与查询脚本必须分离 (build_index.py vs retriever.py):建库是重活(编码几万条),只在资料变更时跑;查询是轻活(编码一条),每次提问都跑。混在一起会导致每次提问都重新编码整个库------这在客户现场是最典型的性能事故。

实操步骤 5:Embedding 封装

kb/embedder.py:

复制代码
"""Embedding 封装:全项目只用这一个入口,保证【建库和查询用同一个模型】"""
import os
os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")

import numpy as np
from FlagEmbedding import BGEM3FlagModel

MODEL_NAME = "BAAI/bge-m3"
USE_FP16 = False          # 无 GPU 必须为 False
_model = None


def get_model() -> BGEM3FlagModel:
    global _model
    if _model is None:
        print(f"⏳ 加载 {MODEL_NAME} ...(首次下载约 1.2GB)")
        _model = BGEM3FlagModel(MODEL_NAME, use_fp16=USE_FP16)
    return _model


def embed(texts: list[str], batch_size: int = 12) -> np.ndarray:
    """文本列表 → 向量矩阵 (n, 1024),已归一化"""
    out = get_model().encode(texts, batch_size=batch_size, max_length=8192)
    return np.asarray(out["dense_vecs"], dtype="float32")


def embed_one(text: str) -> np.ndarray:
    """单条文本 → 向量 (1024,)"""
    return embed([text], batch_size=1)[0]

⚠️ 为什么要单独封装? 因为"建库用 A 模型、查询用 B 模型"是 RAG 项目最经典的致命错误。封装成一个入口,物理上杜绝这个可能。

实操步骤 6:灌库脚本

kb/build_index.py(离线脚本,只在资料变化时跑):

复制代码
"""灌库脚本:手册 Markdown → 分块 → BGE-M3 向量 → Chroma
用法:python -m kb.build_index
"""
import re
from pathlib import Path
import chromadb

from .embedder import embed
from .chunker import chunk_by_sentences          # Day64 验证过的最优策略

CHROMA_DIR = Path("./chroma_db")
COLLECTION = "manual"
MANUAL = Path("data/cleaned/manual_a3.clean.md")

FALLBACK = """
# A3 型注塑机操作与维护手册
## 4.3 冷却系统维护
冷却水进水压力正常范围为 0.3~0.5MPa,低于 0.25MPa 时必须停机检查。
停机后应先关闭进水阀,挂牌上锁,再拆卸管路进行疏通。疏通完成后需进行 0.6MPa 保压试验,保压 10 分钟无渗漏方可恢复运行。
## 6.1 加热系统
加热圈共 4 段,单段电阻值标准 18±2 欧姆。固态继电器击穿后加热圈会持续通电。更换加热圈前必须切断主电源并挂牌上锁(LOTO)。
## 3.2 料筒超温处置
A3 注塑机料筒设定温度 80℃,超过 85℃ 触发高温告警。禁止在料筒温度未降至 60℃ 以下时打开防护罩。
## 7.4 液压系统
液压油每 500 小时更换一次。伺服阀阀芯卡滞会导致锁模力下降,表现为产品飞边增多。
"""


def split_with_metadata(md: str, source: str) -> list[dict]:
    """按 Markdown 章节切块,并抽出元数据(章节名 + 设备号)"""
    blocks, cur_title, buf = [], "全文开头", []

    def flush():
        body = "\n".join(buf).strip()
        if not body:
            return
        # 大块再按句子切(Day64 结论:按句子组装,不在句中断开)
        parts = chunk_by_sentences(body, chunk_size=500, overlap=1)
        for i, p in enumerate(parts):
            # ⭐ 上下文前缀:把章节标题塞回块里,给向量补上"归属"语义
            m = re.search(r"([A-Z]\d+)", cur_title + " " + p)
            blocks.append({
                "text": f"【{cur_title}】\n{p}",
                "chapter": cur_title,
                "device": m.group(1) if m else "UNKNOWN",
                "part": f"{i+1}/{len(parts)}",
                "source": source,
            })

    for ln in md.splitlines():
        m = re.match(r"^(#{1,6})\s+(.*)", ln)
        if m:
            flush()
            buf, cur_title = [], m.group(2).strip()
        else:
            buf.append(ln)
    flush()
    return blocks


def main():
    if MANUAL.exists():
        md = MANUAL.read_text(encoding="utf-8")
        src_name = MANUAL.name
        print(f"✅ 读取手册:{MANUAL}({len(md)} 字符)")
    else:
        md, src_name = FALLBACK.strip(), "sample"
        print("⚠️ 未找到清洗后的手册,使用内置示例(请先完成 Day63)")

    blocks = split_with_metadata(md, source=src_name)
    print(f"📦 共切出 {len(blocks)} 个块,平均 "
          f"{sum(len(b['text']) for b in blocks) // len(blocks)} 字")

    print("⏳ 正在向量化...(CPU 上约需几十秒到几分钟)")
    vecs = embed([b["text"] for b in blocks])

    client = chromadb.PersistentClient(path=str(CHROMA_DIR))
    col = client.get_or_create_collection(
        name=COLLECTION, metadata={"hnsw:space": "cosine"})

    # 先删同 source 旧数据 → 实现增量更新(改一页不用重建整库)
    old = col.get(where={"source": src_name})["ids"]
    if old:
        col.delete(ids=old)
        print(f"🗑️  已清除该来源的旧数据 {len(old)} 条")

    col.add(
        ids=[f"{src_name}-{i:04d}" for i, b in enumerate(blocks)],
        documents=[b["text"] for b in blocks],
        embeddings=[v.tolist() for v in vecs],
        metadatas=[{"source": b["source"], "chapter": b["chapter"],
                    "device": b["device"], "part": b["part"]} for b in blocks],
    )
    print(f"✅ 灌库完成,当前库内共 {col.count()} 条,落盘于 {CHROMA_DIR.resolve()}")


if __name__ == "__main__":
    main()

运行:

复制代码
python -m kb.build_index

✅ 读取手册:data/cleaned/manual_a3.clean.md(1082 字符)
📦 共切出 6 个块,平均 214 字
⏳ 正在向量化...
✅ 灌库完成,当前库内共 6 条
   落盘位置:E:\...\fde-ai\chroma_db

实操步骤 7:检索封装

kb/retriever.py:

复制代码
"""检索封装:问题 → 相关块(带距离与元数据)"""
from pathlib import Path
import chromadb

from .embedder import embed_one

CHROMA_DIR = Path("./chroma_db")
COLLECTION = "manual"

# 经验阈值:cosine 距离大于这个值就认为"不相关",触发拒答
# (距离 = 1 - 余弦相似度,所以 0.5 对应相似度 0.5;相关片段通常 < 0.4)
MAX_DISTANCE = 0.55


def _collection():
    client = chromadb.PersistentClient(path=str(CHROMA_DIR))
    return client.get_or_create_collection(name=COLLECTION,
                                           metadata={"hnsw:space": "cosine"})


def retrieve(question: str, top_k: int = 4, device: str | None = None) -> list[dict]:
    """检索相关块;device 不为空时按设备过滤"""
    col = _collection()
    qv = embed_one(question)

    kwargs = {"query_embeddings": [qv.tolist()], "n_results": top_k,
              "include": ["documents", "metadatas", "distances"]}
    if device:
        kwargs["where"] = {"device": device}

    res = col.query(**kwargs)
    hits = []
    for i, doc in enumerate(res["documents"][0]):
        dist = float(res["distances"][0][i])
        if dist > MAX_DISTANCE:
            continue                      # 太远,视为不相关
        md = res["metadatas"][0][i]
        hits.append({
            "id": res["ids"][0][i],
            "text": doc,
            "distance": dist,
            "similarity": 1 - dist,       # 换算回相似度,方便阅读
            "source": md.get("source"),
            "chapter": md.get("chapter"),
            "device": md.get("device"),
        })
    return hits


def show(question: str, top_k: int = 4, device: str | None = None):
    """带打印的检索,方便调试观察"""
    print(f"\n🔍 问题:{question}" + (f"  (限定设备 {device})" if device else ""))
    for h in retrieve(question, top_k, device):
        print(f"   相似度 {h['similarity']:.3f} | {h['device']} | {h['chapter']}")
        print(f"      {h['text'][:70].replace(chr(10), ' ')}...")

先单独验证检索:

复制代码
python -c "from kb.retriever import show; show('冷却水压力低于多少必须停机?')"

🔍 问题:冷却水压力低于多少必须停机?
   相似度 0.812 | A3 | 4.3 冷却系统维护
      【4.3 冷却系统维护】冷却水进水压力正常范围为 0.3~0.5MPa,低于 0.25MPa 时必须停机检查。...
   相似度 0.634 | A3 | 4.3 冷却系统维护
      【4.3 冷却系统维护】停机后应先关闭进水阀,挂牌上锁,再拆卸管路进行疏通。...
   相似度 0.521 | A3 | 3.2 料筒超温处置
      【3.2 料筒超温处置】A3 注塑机料筒设定温度 80℃,超过 85℃ 触发高温告警。...

📌 读结果:0.812 很相关、0.521 已偏弱(同主题边缘);排序是对的 (讲压力的第 1,讲超温的第 3);chapter 元数据带回来了,引用溯源有着落。

实操步骤 8:问答(检索 + 生成 + 引用)

kb/answer.py:

复制代码
"""问答:检索 → 组装 Prompt → DeepSeek 生成 → 带引用输出"""
import os
from dotenv import load_dotenv
from openai import OpenAI

from .retriever import retrieve

load_dotenv()
client = OpenAI(api_key=os.getenv("DEEPSEEK_API_KEY"),
                base_url="https://api.deepseek.com")

SYSTEM = """你是工厂设备运维助手,依据【参考资料】回答一线维修人员的问题。

【铁律】
1. 只使用【参考资料】中的信息,禁止使用资料外的知识作答
2. 每个结论后用 [n] 标注它来自哪条资料(n 为资料编号)
3. 资料中没有的内容,明确回答"资料中未提及",禁止推测和编造
4. 涉及安全的操作(断电、挂牌上锁、防护罩等)必须原样写出
5. 回答简洁,用编号步骤,每步不超过两句话

【输出格式】答案正文(带 [n])--- 参考来源:[n] 《来源文件》 章节名"""


def answer(question: str, top_k: int = 4, device: str | None = None) -> dict:
    hits = retrieve(question, top_k=top_k, device=device)
    if not hits:
        return {"answer": "抱歉,现有手册中没有找到相关内容。"
                          "请补充资料或联系设备工程师。",
                "sources": [], "grounded": False}

    context = "\n\n".join(
        f"[{i+1}] 来源:{h['source']} | 章节:{h['chapter']} | 设备:{h['device']}\n{h['text']}"
        for i, h in enumerate(hits))

    resp = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"【参考资料】\n{context}\n\n【问题】{question}"},
        ],
        temperature=0,
        max_tokens=800,
    )
    return {
        "answer": resp.choices[0].message.content,
        "sources": [{"n": i + 1, "source": h["source"],
                     "chapter": h["chapter"], "score": round(h["similarity"], 3)}
                    for i, h in enumerate(hits)],
        "grounded": True,
        "tokens": resp.usage.total_tokens,
    }

命令行入口 day65_ask.py:

复制代码
"""Day65:手册问答命令行"""
import sys
from kb.answer import answer

if __name__ == "__main__":
    question = " ".join(sys.argv[1:]) or "冷却水压力低于多少必须停机?"
    r = answer(question)
    print(f"\n问:{question}\n")
    print(r["answer"])
    print("\n------ 召回来源 ------")
    for s in r["sources"]:
        print(f"  [{s['n']}] {s['source']} · {s['chapter']}  相似度 {s['score']}")
    print(f"\n(消耗 {r.get('tokens', 0)} tokens, grounded={r['grounded']})")

python day65_ask.py "料筒超温怎么处理?"

问:料筒超温怎么处理?

1. 确认料筒温度是否持续超过 85℃(设定值 80℃)[1]
2. 检查冷却水管路是否结垢堵塞,冷却水压力是否低于 0.3MPa [1]
3. 检查固态继电器是否击穿导致加热圈持续通电 [1][3]
注意:料筒温度未降至 60℃ 以下时禁止打开防护罩 [2]

------ 召回来源 ------
  [1] manual_a3.clean.md · 4.3 冷却系统维护  相似度 0.78
  [2] manual_a3.clean.md · 3.2 料筒超温处置  相似度 0.71
  [3] manual_a3.clean.md · 6.1 加热系统  相似度 0.63

(消耗 742 tokens, grounded=True)

回到 Day 61 的开场 :同样的问题,Day 61 时模型是凭空编一套流程 ;今天它照着手册答,还标出了每一条来自哪个章节。

📌 这就是"知识库问答 v0.2"。请把它和 Day 61 存档的那三份输出放在一起对比,这是你本周最有说服力的成果。

实操步骤 9:挂到 FastAPI(合进主线系统)

kb/api.py,与第二阶段的工单系统并列(Pydantic v2 + FastAPI 全部复用第二阶段所学):

复制代码
"""把手册问答接进 FastAPI(v0.2 的接口)"""
from fastapi import FastAPI
from pydantic import BaseModel, Field
from .answer import answer

app = FastAPI(title="智能运维助手 v0.2", version="0.2.0")


class AskRequest(BaseModel):
    question: str = Field(..., min_length=2, max_length=200,
                          examples=["A3 冷却水压力低于多少要停机?"])
    device: str | None = Field(None, description="限定设备号,如 A3;None 表示全库检索")
    top_k: int = Field(4, ge=1, le=10)


class Source(BaseModel):
    n: int
    source: str
    chapter: str
    score: float


class AskResponse(BaseModel):
    question: str
    answer: str
    grounded: bool
    sources: list[Source]


@app.post("/api/manual/ask", response_model=AskResponse, tags=["知识库问答"])
def ask_manual(req: AskRequest):
    r = answer(req.question, top_k=req.top_k, device=req.device)
    return AskResponse(question=req.question, answer=r["answer"],
                       grounded=r["grounded"],
                       sources=[Source(**s) for s in r["sources"]])

uvicorn kb.api:app --reload --port 8000

# 全库问答
curl -X POST http://localhost:8000/api/manual/ask -H "Content-Type: application/json" \
  -d '{"question":"液压油多久换一次?"}'
# 限定设备(走 where 过滤)
curl -X POST http://localhost:8000/api/manual/ask -H "Content-Type: application/json" \
  -d '{"question":"冷却水压力要求是多少?","device":"A3"}'

打开 High Performance Web Crawler API - Swagger UI 看自动生成的文档,接口自解释。

实操步骤 10:Docker 部署 Qdrant(面向生产)

Chroma 是嵌入式的,生产推荐 Qdrant。平台类工具一律 Docker 部署:

复制代码
# 一条 docker run 起 Qdrant(6333=HTTP+Dashboard,6334=gRPC)
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 \
  -v ${PWD}/qdrant_storage:/qdrant/storage \
  qdrant/qdrant:latest

或写进 compose,与第二阶段的工单系统同栈编排:

复制代码
services:
  qdrant:
    image: qdrant/qdrant:latest
    container_name: qdrant
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_storage:/qdrant/storage
    restart: unless-stopped

volumes:
  qdrant_storage:

docker compose up -d qdrant       # 打开 http://localhost:6333/dashboard

💡 Qdrant 的 Python 客户端是 QdrantClient(host, port) + recreate_collection + upsert(points=[PointStruct(id, vector, payload)]) + search(query_vector, limit)。看出规律了吗?collection / vector / payload(metadata) / id / top-k / 距离,概念跟 Chroma 一一对应。今天学的概念到 Qdrant 完全平移,第 10 周 Day 66 只是换个客户端。


📖 五、本周复盘:知识地图

Day 课时 核心知识点 产出
61 3.2.1 RAG 全景 三硬伤(截止/私域/幻觉);八步链路;不全塞的四条理由 最小 RAG 脚本 + FastAPI 接口
62 3.2.4 Embedding 语义地图;余弦相似度;BGE-M3(1024 维/8192/三合一) 相似度矩阵 + 向量版检索器
63 3.2.2 文档解析 PDF 五坑;MinerU / Docling / PyMuPDF;表格七项质检 full.md → 清洗脚本 → *.clean.md
64 3.2.3 文本分块 三约束;固定/重叠/结构/语义 + 父子分块;上下文前缀 四策略对比实验 + 参数经验值
65 3.2.5 向量数据库 ANN/HNSW;四选型;Chroma 持久化与 where 过滤 手册向量库 + 带引用问答 = v0.2

主线项目进展

复制代码
  第二阶段工单闭环 ──► v0.1(分类/提取/报告)──► v0.2(+ 知识库问答)
  同一个 FastAPI 服务,能力一层层叠上去
  curl /api/manual/ask  ← 今天新加的接口

📋 周末验收清单

# 验收项 达标标准 自评
1 全局认知 能脱稿画出 RAG 八步链路并解释每步作用 ☐
2 Embedding 能解释"为什么语义近的句子向量近",跑通 BGE-M3 ☐
3 文档解析 真实 PDF 转出文本,表格不乱码(有质检记录) ☐
4 分块 能用数据说明哪种分块更适合设备手册(有对比表) ☐
5 向量库 提问能召回相关手册段落,相似度 > 0.6 ☐
6 元数据 每条 chunk 带 source / chapter / device ☐
7 引用溯源 答案带章节标注,能核对到原文 ☐
8 拒答 问资料外的问题能触发"资料中未提及" ☐
9 接口 /api/manual/ask 可调用,/docs 有文档 ☐
10 安全 Key 在 .env,.gitignore 含 .env 与 chroma_db/ ☐

📊 向量数据库速查表

Chroma API 速查

操作 代码
持久化客户端 chromadb.PersistentClient(path="./chroma_db")
建/取集合 client.get_or_create_collection(name="manual", metadata={"hnsw:space":"cosine"})
灌数据 col.add(ids=[...], documents=[...], embeddings=[...], metadatas=[...])
查询 col.query(query_embeddings=[vec], n_results=5)
带过滤查询 col.query(..., where={"device": "A3"})
按 id 取 col.get(ids=["c1"], include=["documents","metadatas"])
更新/插入 col.upsert(...)(参数同 add)
删除 col.delete(ids=[...]) / col.delete(where={...})
条数 / 翻看 col.count() / col.peek(limit=5)
列/删集合 client.list_collections() / client.delete_collection("manual")

距离空间与阈值

hnsw:space 含义 什么时候用
cosine 余弦距离 = 1 - 余弦相似度 默认推荐,语义检索通用
l2 欧氏距离 关心绝对数值差(少见)
ip 内积距离 = -(内积) 向量已归一化且追求速度
距离 相似度 判定
0.10 0.90 高度相关 ✅
0.25 0.75 相关 ✅
0.40 0.60 弱相关,要注意 ⚠️
0.55 0.45 不相关 ❌(建议设为拒答阈值)

常见翻车与解法

❌ 症状 原因 ✅ 解法
中文检索效果极差 用了 Chroma 默认嵌入模型(英文) 自己传 BGE-M3 向量
重复跑脚本报"已存在" 用了 create_collection 用 get_or_create_collection
检索结果全是同一段 ids 重复被覆盖 ids 必须唯一(source-序号)
距离全是 0.99 左右 查询与库里向量不是同一模型出的 统一走 kb/embedder.py
where 过滤没生效 字段名拼错 / 该条没这个字段 col.peek() 看真实 metadata

📝 本课小结

知识点 一句话记住
向量库两件事 存(向量+原文+元数据)+ 查(ANN 近似最近邻)
ANN / HNSW 用索引图跳过绝大多数候选,O(log n),换来毫秒级
一条记录四件套 id / embedding / document / metadata
metadata 价值 既用于过滤(只看 A3),也用于溯源(标 P47)
选型 Chroma 原型 / Qdrant 生产 / Milvus 大规模 / PGVector 已有 PG
距离 vs 相似度 Chroma 返回距离,越小越像;相似度 = 1 - 距离
拒答阈值 余弦距离 > 0.55 视为不相关,触发"资料中未提及"
中文坑 Chroma 默认模型是英文的,必须自己传 BGE-M3 向量
幂等建库 get_or_create_collection 让脚本能反复跑
增量更新 按 source 先 delete 再 add,改一页不用重建整库
脚本分离 落库脚本重、查询脚本轻,绝不能混在一起跑
v0.2 手册检索 + 带引用回答 + FastAPI 接口

🧠 核心认知 :向量数据库把"语义检索"从研究问题 变成了工程问题 ------你不再需要关心怎么算相似度,只需要关心三件事:存什么(document)、标注什么(metadata)、用什么模型编码(embedding) 。这三件事的质量,决定了你系统的天花板;而向量库本身,只是一个越来越成熟的、可以替换的零件。FDE 的价值不在于会不会用 Chroma(那只是几行 API),而在于知道该把什么存进去、标什么元数据、以及什么时候该换 Qdrant。


📋 课后练习

练习 1:扩充知识库并验证增量更新(约 45 分钟)

  1. 再准备一份手册(manual_b2.pdf,或把示例手册复制改成 B2 的参数),走完 Day63 的解析与清洗,得到 manual_b2.clean.md。

  2. 改 build_index.py 支持批量灌入多份手册 (遍历 data/cleaned/*.clean.md),灌进同一个 collection。

  3. 验证增量更新:把 manual_a3.clean.md 里 0.25MPa 改成 0.28MPa,只重跑 A3 的灌库,确认 A3 更新了、B2 没被动过。

  4. 用 col.count() 和 col.peek() 确认两个来源的数据都在。

练习 2:元数据过滤实战(约 40 分钟)

  1. 提问"冷却水压力要求是多少?",分别跑不带过滤 、device="A3"、device="B2" 三次,对比返回的章节与相似度,确认过滤生效。

  2. 给 metadata 加两个字段:acl(可访问角色,如"设备部"/"全员")和 doc_type("手册"/"工单"/"SOP"),重新灌库。

  3. 写一个 retrieve_by_role(question, role),内部用 where={"acl": role} 过滤------这是第 10 周权限过滤的雏形。

  4. 验证:用一个没有权限的角色提问,确认检索不到受限内容。

练习 3:端到端验收 + 本周成果归档(约 45 分钟)

  1. 用 Day 61 存档的三个"私有问题"重新问 v0.2,对比 Day 61(编造)与今天(有据 + 有引用)的输出,做成对比表存档。

  2. 按"周末验收清单"10 项逐条自评,把不达标的两项补做。

  3. 把 kb/、data/cleaned/、Day64 的分块报告整理到一个文件夹,写一份 100 字周总结。

  4. 思考题(写下来,第 10 周回来对照):现在有三个短板------① 设备型号专名检索会漏(Day62 观察到);② Top-3 里混着弱相关块;③ 引用只能标到章节,标不到页码。你觉得该怎么治?


🔭 下节预告

本周你搭出了一条能跑通的 RAG 链路。但"能跑通"和"生产可用"之间,还差第 10 周的整整一周。

先说清楚现在的三个短板:① 专名会漏 ------"TK-108"vs"TK-115"相似度只差 0.03,向量检索分不清精确编号;② 排序不精 ------Top-3 里混着相似度 0.5 的弱相关块,挤掉了真正该进来的;③ 溯源太粗------只能标到章节,标不到页码。

第 10 周(Day 66-70)逐个击破,主题是**「检索、重排与引用溯源 --- 让答案找得准、说得清出处」**:

Day 主题 做什么
Day 66 Qdrant 生产级向量库 Docker 部署 Qdrant,把今天的 Chroma 库迁过去,对比两者的检索表现与运维差异
Day 67 BM25 + 混合检索 关键词检索补上向量检索的短板;用上 BGE-M3 的 sparse_vecs,让"A3""TK-108"这类专名精确命中
Day 68 Reranker 重排 用 BGE-Reranker 对 Top-20 做精排,把真正相关的顶到 Top-3,治"排序不精"
Day 69 引用溯源 + 权限过滤 答案精确到页码/行号并可点击回原文;用 metadata 实现"谁能看哪些 chunk"
Day 70 RAGAS 评测 + 收官 建黄金问题集,跑 faithfulness / context precision 等指标,用数据证明你的改进有效

一句话:第 9 周让系统"能答",第 10 周让系统"答得准、说得清出处、还能量化证明"。

好好休息,明天进入生产级检索。本周收官快乐 🎉


🌍附录:前置课程列表

阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知

【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客

【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客

【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客

【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客

【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客

【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客

【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客

【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客

【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客

【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客

【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客

【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文

【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客

【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客

【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客

FDE 基础概念

【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客

【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客

【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客

【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客

【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客


阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础

【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客

【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客

【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客

【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客

【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客

【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客

【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客

FastAPI入门到进阶

【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客

【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客

【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客

SQL基础

【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客

【FDE系列】阶段2:Day 33:进阶查询 --- 窗口函数与 CTE-CSDN博客

【FDE系列】阶段2:Day 34:数据清洗 --- 把脏数据捋干净-CSDN博客

【FDE系列】阶段2:Day 35:Python + SQL --- 工单接入 MySQL + 本周收官-CSDN博客

Linux基础

【FDE系列】阶段2:Day 36:Linux 入门与文件操作 --- 扔掉鼠标的第一天-CSDN博客

【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客

【FDE系列】阶段2:Day 38:Shell 脚本 --- 把命令串起来自动跑-CSDN博客

【FDE系列】阶段2:Day 39:Linux 综合实战 --- 让服务无人值守-CSDN博客

【FDE系列】阶段2:Day 40:Shell 进阶 --- 生产级脚本与本周收官-CSDN博客

Docker

【FDE系列】阶段2:Day 41:Docker 入门 --- 把环境装进盒子-CSDN博客

【FDE系列】阶段2:Day 42:Dockerfile 实战 --- 把你的应用打包成镜像-CSDN博客

【FDE系列】阶段2:Day 43:Docker Compose --- 多容器一键编排-CSDN博客

【FDE系列】阶段2:Day 44:Nginx 反向代理 + Git 版本控制-CSDN博客

【FDE系列】阶段2:Day 45:综合实战 --- Docker + Nginx + Git 完整部署与本周收官-CSDN博客

API 集成与系统对接

【FDE系列】阶段2:Day 46:RESTful 设计与认证授权-CSDN博客

【FDE系列】阶段2:Day 47:对接企业系统 --- 飞书 / 钉钉 API-CSDN博客

【FDE系列】阶段2:Day 48:Webhook 处理与数据映射-CSDN博客

【FDE系列】阶段2:Day 49:OpenAPI 文档与接口测试-CSDN博客

【FDE系列】阶段2:Day 50:综合项目 --- 设备告警工单闭环系统 & 第二阶段收官 特殊字符-CSDN博客

阶段三:AI 应用技术(含 SDD 方法论)
AI基础:Prompt Engineering 系统训练

【FDE系列】阶段3:Day 51:从聊天窗口到代码 --- 跟 LLM 的第一次握手-CSDN博客

【FDE系列】阶段3:Day 52:Prompt 三板斧 --- 角色、示例与清晰指令-CSDN博客

【FDE系列】阶段3:Day 53:结构化输出 --- 让模型的回答能进数据库-CSDN博客

【FDE系列】阶段3:Day 54:思维链与推理任务 --- 让模型一步步想清楚-CSDN博客

【FDE系列】阶段3:Day 55:综合实战 --- 巡检报告生成器与本周收官 -CSDN博客

【FDE系列】阶段3:Day 56:评测体系入门 --- 建立你的黄金评测集-CSDN博客

【FDE系列】阶段3:Day 57:Promptfoo 实战 --- A/B 对比让数据说话-CSDN博客

【FDE系列】阶段3:Day 58:Prompt 安全 --- 注入、越狱与防护-CSDN博客

【FDE系列】阶段3:Day 59:模板化与追踪 --- Jinja2 与 Langfuse-CSDN博客

【FDE系列】阶段3:Day 60:综合实战 --- 智能工单助手 v0.1 冻结-CSDN博客

RAG 知识检索系统

【FDE系列】阶段3:Day 61:RAG 全景 --- 给模型配一间资料室-CSDN博客

【FDE系列】阶段3:Day 62:Embedding --- 文字是怎么变成向量的-CSDN博客

【FDE系列】阶段3:Day 63:文档解析 --- 把真实 PDF 手册变成可用文本-CSDN博客

【FDE系列】阶段3:Day 64:文本分块 --- 决定检索成败的那一步-CSDN博客

待完成教程:

Agent 框架与开发

Tool Calling 与 MCP

LLM 推理与部署

规范驱动开发与 Agent 工程方法论

阶段四:平台与交付(含 Agent 治理)

阶段五:行业实战与认证

相关推荐
ai小陈1 小时前
深度学习CUDA异步报错定位:从错误堆栈到最小复现
运维·人工智能·深度学习·ai·gpu算力
北京恒星科通刘军1 小时前
灾害监测预警系统中,应急疏散广播的“应急叫应”实际送达率与有效性研究
网络·数据库·人工智能
TK泰妞1 小时前
跨境卖家如何用AI完成TikTok带货视频研究与制作
大数据·人工智能·音视频
HwJack201 小时前
【共创稿事节】HarmonyOS 7手势识别交互:空中手势的语义与容错设计
人工智能·深度学习·华为·交互·harmonyos
goujunwe1 小时前
B2B 设备制造企业 GEO:采购用大模型选型时,采信企业技术参数
大数据·人工智能·制造
Rocky Ding*1 小时前
GPT-6.1 Sol大模型深度解析
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·gpt-6.1 sol
feasibility.1 小时前
把城市压缩成 2067 个盒子:CosFly 无人机跟踪数据管线论文解剖
人工智能·大模型·嵌入式·无人机·vla
ofoxcoding1 小时前
Jev 模型评测深度解析:核心得分、能力局限与最佳适用任务场景
人工智能·算法·机器学习·ai
Light Gao1 小时前
LSTM 与 GRU 详解:从门控机制到架构图与数值计算
人工智能·gru·lstm