Day 62 / 共 68 天 · 阶段 12 实战求职

作品集① 知识库 RAG 助手(端到端)

昨天(Day 61)收尾了部署篇的 AI 网关。今天开始动手做能写进简历的第一件作品:给一堆文档(如公司手册、产品说明)做一个"问它就答、还能给出处"的助手。我们会走完需求 → 摄入 → 检索 → 问答 → 评测 → 部署六步,把阶段 4(RAG)、阶段 9(评测)、阶段 11(部署)全部串起来,并留下一套可复用代码骨架。明天(Day 63)做第二件:会调工具的多工具 Agent。

📍 你在阶段 12(实战求职 D62-68)的位置
D62 作品集①RAG D63 作品集②工具 D64 作品集③多智能体 D65 开源包装 D66 简历 D67-68 面试冲刺
💡 用一个类比兜住今天(今天全程沿用「开一家私人图书馆问答台」的世界观) 做 RAG 助手 = 开一家能回答问题的私人图书馆需求 = 想清楚"读者是谁、来问什么"(先别急着装修);摄入(读切嵌存) = 进书 → 把厚书拆成一页页便签 → 给每张便签贴上"主题标签" → 上架编号,方便日后秒找;检索 = 读者提问,管理员按标签飞快抽出最相关的几张便签;问答 = 图书管理员照着这几张便签、用人话组织出答案,并告诉你"这话出自哪本书第几页";评测 = 请一位神秘顾客来打分,证明这家馆"真能答对";部署 = 把图书馆开到街边,挂个门牌让人能进来问。今天你就是这家馆的馆长。
L01

先定需求:别急着写代码

🤔 痛点新手一上来就 pip install 猛敲代码,做到一半发现"我到底给谁用、答不对算谁的锅、怎么算成功",全没想过。结果东西能跑却讲不出价值——面试时你说不清"解决了什么问题",项目就白做了。
💡 本质作品集项目和练习最大的区别:它要能讲成一个"问题→方案→结果"的故事(这正是 Day 66 简历包装的原料)。开馆前先花 10 分钟填一张"需求卡":用户是谁、痛点、范围、成功标准。想清楚了,后面每一步都有靶子。
📝 举个例子:一张填好的需求卡(照抄改成你的) 项目名:员工手册问答助手
用户:新入职员工
痛点:制度散在 20 个 PDF 里,问 HR 又不好意思反复打扰
范围(做什么):回答"考勤/请假/报销"类问题,并给出制度出处
不做(划边界):不处理个人薪资等隐私、不聊闲天
成功标准:20 道常见问题,答对率 ≥ 85% 且都能给出处、单次回答 < 3 秒
👶 为什么"不做什么"也要写?因为范围不划清,你会永远做不完(用户什么都想问)。写清"不做隐私、不闲聊",既是产品边界,也是安全边界(呼应 Day 20 注入防护、Day 54 护栏)。面试时你能说"我主动划了边界",这是工程成熟度的体现。
L02

六步流水线全景:先看地图再上路

🤔 痛点RAG 涉及切分、嵌入、向量库、检索、生成……单看每个都懂(阶段 4 学过),但串起来做一个完整项目,脑子里没有一张"从文档到答案"的总流程图,很容易做着做着就迷路。
💡 本质整个项目就两条流水线:离线摄入线(建馆时把书变成可检索的便签,只需跑一次)和在线问答线(读者每次提问都走一遍)。先把这张图刻进脑子,后面每写一段代码,你都知道它是哪一步。
RAG 两条流水线 离线摄入线(建馆,跑一次) 读文档 切块chunk 嵌入embed 存进向量库 在线问答线(读者每次提问) 用户提问 检索top-k 拼进prompt LLM生成+出处 检索从向量库里捞便签 🎯 最后再套一条"评测线"给整套打分(L05),达标才敢部署(L06)
图注:绿线只在建馆时跑一次;蓝线每次提问跑一遍。写代码时,时刻自问"这在哪条线上"。

RAG 每一步的原理细节这门课在阶段 4 讲过,想再深挖切分/检索/评测的工程实现,可以去 LlamaIndex 精讲(选学,学完回来继续 Day 62):

🔗 深入《LlamaIndex RAG 20 天精讲》→
L03

摄入线:读→切→嵌→存(可复用骨架)

🤔 痛点把一堆 PDF 变成"能检索的东西",听着要写很多代码。其实用现成库(LlamaIndex/LangChain)几十行就能搞定,关键是留意两个坑:切多大(Day 22)和存不存出处(答案要能溯源)。
💡 本质摄入就是把"厚书"变成"贴好标签、编好号的便签盒"。切块 = 撕成便签(别太大也别太碎);嵌入 = 给每张便签算一串"语义坐标"(Day 23);存 = 连同"来自哪个文件"一起入库,这样将来才能给出处。
# 摄入骨架(以 LlamaIndex 为例,几行建好一座可检索的图书馆)
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter

# 1) 读:把 docs/ 文件夹里所有文档读进来(自动带上文件名当出处)
docs = SimpleDirectoryReader("docs").load_data()

# 2) 切:每块约 500 字、相邻块重叠 50 字(重叠防止把一句话拦腰切断)
splitter = SentenceSplitter(chunk_size=500, chunk_overlap=50)

# 3+4) 嵌入 + 存:建索引时自动完成"算向量→入向量库"
index = VectorStoreIndex.from_documents(docs, transformations=[splitter])

# 把索引存到磁盘,下次直接加载,不用重新摄入(离线线只跑一次!)
index.storage_context.persist("storage")
print("✅ 图书馆建好了,便签都上架编号完毕")
📝 举个例子:切块大小的直觉 切太大(如 3000 字一块):检索到的便签里塞了一堆无关内容,答案被稀释、还费 Token。切太碎(如 80 字):一个完整规定被切散,检到半句话答不全。先用 500 字 + 50 重叠起步,评测(L05)发现答不好再调——这就是工程,靠数据调,不靠拍脑袋。
L04

问答线:检索 + 生成 + 给出处

🤔 痛点光把相关便签找出来还不够。用户要的是"一句人话答案",而且——面试和真实场景都极看重——得能说清这话是哪来的,否则用户凭啥信?没出处的 RAG 就是"看着专业的瞎编"。
💡 本质问答 = 检索(捞出最相关的 k 张便签)→ 把便签塞进 prompt 当"参考资料"→ 让 LLM只根据参考资料组织答案,并附上出处。核心一句 prompt 纪律:"只用下面的资料回答,资料里没有就说不知道"——这是防幻觉(Day 53)的第一道闸。
# 问答骨架:加载上一步建好的库,问一句,拿到答案 + 出处
from llama_index.core import StorageContext, load_index_from_storage

index = load_index_from_storage(StorageContext.from_defaults(persist_dir="storage"))

# 关键:给一条"只依据资料、否则说不知道"的系统纪律
SYSTEM = "你是文档助手。只根据检索到的资料回答;资料没提到就回答'资料中未找到',不要编。"

engine = index.as_query_engine(similarity_top_k=4, system_prompt=SYSTEM)  # 捞前4张便签
resp = engine.query("试用期怎么请假?")

print(resp)                                   # 人话答案
for node in resp.source_nodes:                # 出处:这答案参考了哪些便签
    print("📎 出处:", node.metadata.get("file_name"), "  相关度:", round(node.score, 2))

👶 小白:为什么一定要 top_k?直接把所有文档喂给大模型不行吗?

👨‍🏫 老师:不行,有三个原因。①上下文窗口有限(Day 10、Day 18),塞不下几十个 PDF;②塞太多,真正相关的那句被淹没,模型反而"迷失在中间"(lost in the middle,Day 18);③费钱费时——你为一堆没用的内容付 Token(Day 13)。所以先检索缩小到最相关的几张便签,再交给模型,是又准又省的关键。图书管理员不会把整馆的书都搬给你,只抽最相关那几本。

L05

评测:请"神秘顾客"证明它真好用

🤔 痛点"感觉答得还行"是新手最大的坑。面试官一句"你怎么知道它好用、准确率多少",就把没评测的项目问穿了。没有数字,你的作品就只是个 demo,不是工程。
💡 本质评测 = 请一位"神秘顾客"带着标准答案来考馆(呼应阶段 9)。做法:攒一个 golden set(20~30 个"问题+期望要点")→ 让助手逐个回答 → 用两把尺子量:检索命中率(该找的便签找到没,Day 27)和答案忠实度(有没有照资料说、没编,可用 LLM-as-Judge,Day 50)。得出一个能写进简历的数字。
# 极简评测骨架:跑一批问题,统计答对率
golden = [
    {"q": "试用期能请年假吗?",   "must_have": "试用期不享受年假"},
    {"q": "报销单几天内交?",     "must_have": "15个工作日"},
    # ... 攒够 20~30 条,覆盖常见问题和边角问题
]
hit = 0
for case in golden:
    answer = str(engine.query(case["q"]))
    ok = case["must_have"] in answer          # 简版判分:期望要点是否出现在答案里
    hit += ok
    print(("✅" if ok else "❌"), case["q"])
print(f"答对率:{hit}/{len(golden)} = {hit/len(golden):.0%}")   # ← 这个数字写进简历
👶 判分这么粗糙(只查关键词)靠谱吗?入门够用!先用"关键词是否出现"快速拿到一个基线数字,能跑起回归就赢过 90% 只做 demo 的人。想更准,再升级成 LLM-as-Judge(Day 50):让另一个模型按 rubric 打分"这答案是否忠于资料、是否答到点"。关键是有评测这件事本身,以及"答不好→调切块/top_k→再测"的闭环。
📝 举个例子:评测驱动改进(写进简历的故事) 初版答对率 68% → 看错题发现"制度被切散了" → 把 chunk_size 从 800 调到 500、top_k 从 3 调到 5 → 再测 86%。这一段"用数据发现问题、定位、改进、再验证"的经历,就是面试最想听的,比"我用了向量数据库"这种名词堆砌值钱十倍。
L06

部署 + 项目结构骨架

🤔 痛点代码在你电脑上能跑,可面试官/招聘方要的是"能点开就用"。总不能让人 clone 下来还得配一堆环境。得给它套个 Web 接口、能一键启动,最好还能截图/录屏演示。
💡 本质用 FastAPI(Day 09)把问答函数包成一个 /ask 接口 → 用 Docker(Day 57)打包成"到哪都能跑的盒子" → 本地或云上(Day 60)跑起来。前面学的部署技能,到这里全用上了。下面给一套清爽的项目目录,直接照搬。
# 可复用项目骨架(照这个建目录,README 里也这么写)
rag-assistant/
├── docs/               # 放你的知识文档(PDF/txt/md)
├── ingest.py           # 摄入线:读切嵌存 → 生成 storage/(L03)
├── app.py              # 问答线:FastAPI 暴露 /ask 接口(L04 + Day09)
├── eval.py             # 评测:跑 golden set 出答对率(L05)
├── golden.json         # 评测题库(问题 + 期望要点)
├── requirements.txt    # 依赖清单(Day05)
├── Dockerfile          # 打包成镜像(Day57)
└── README.md           # 项目说明 + 效果数字 + demo 截图(Day65)
# app.py —— 把问答包成一个 HTTP 接口(配合 uvicorn app:app 启动)
from fastapi import FastAPI
from pydantic import BaseModel
# ...(此处加载 index、engine,同 L04)

app = FastAPI()
class Q(BaseModel):          # 请求体:一个问题字符串
    question: str

@app.post("/ask")
def ask(q: Q):
    resp = engine.query(q.question)
    return {                                   # 答案 + 出处一起返回,前端好展示
        "answer": str(resp),
        "sources": [n.metadata.get("file_name") for n in resp.source_nodes],
    }
# 浏览器打开 /docs 就能在线试问(Day09 学过的自动文档)
加分项:接一个最简单的 Web 前端(甚至一个 HTML 输入框)、把答对率写进 README、录一段 30 秒 demo。这些让"能跑的代码"变成"看得见的作品"——正是 Day 65 要专门教的事。
L07

今日小结 + 动手 10 分钟

🧠 今天你应该能回答

  • 做作品集为什么要先填"需求卡"?"不做什么"为什么重要?
  • 画出 RAG 的两条流水线(离线摄入 / 在线问答),说清每步干啥?
  • 切块为什么别太大也别太碎?为什么答案一定要带出处?
  • 问答为什么先 top_k 检索再喂模型,而不是把全部文档丢进去?
  • 怎么用 golden set 得出一个"答对率"数字?评测驱动改进的故事怎么讲?

✋ 动手 10 分钟:先把"需求卡 + 骨架 + 5 道题库"落地

今天不求跑通全流程,先把项目"立起来"(真正的编码建议课后花 2~3 小时补完):

  1. 照 L01 的模板,给你想做的知识库填一张需求卡(用户/痛点/范围/不做/成功标准),存进 README.md
  2. 照 L06 的目录,mkdir 建出 rag-assistant/ 骨架(空文件先建着,心里有结构)。
  3. golden.json 里写 5 条评测题(问题 + 一句期望要点)。哪怕还没代码,先想清"什么样算答对"。
mkdir -p rag-assistant/docs && cd rag-assistant
touch ingest.py app.py eval.py golden.json requirements.txt Dockerfile README.md
echo '这是我的第一件 Agent 作品集' > README.md
ls -la          # 看一眼骨架,成就感拉满
明日预告 · Day 63:作品集第二件——多工具 Agent。RAG 助手只会"查资料答题",明天让 Agent 学会真正"动手":调天气 API、查库存、发邮件。核心是把 Day 29 的 Function Calling 和 Day 30 的安全工具设计、错误处理落进一个能演示的项目里,让"会说"升级成"会做"。
← Day 61 · AI 网关 Day 63 · 作品集② 多工具 Agent →