夜雨聆风学习资料网

ARTICLE · 1126516

国庆7天|Day5·让它读你自己的PDF

国庆7天|Day5·让它读你自己的PDF

一、系列导航

这是第 5 天。7 篇只养一个助理——小喵助理,新来的进公众号主页「合集」看全部。

前四天一直在敲代码,今天也一样:加一个文件、一个函数,它就能读你的文件了。

二、昨日回顾:它会干活了,但没看过你的文件

昨天它能把一件大事拆成几步办完,最后落成一份文件。

可你问它"依据某份文件里的规定,这种情况该怎么处理",它就露馅了——答得又快又顺,

但那全是凭印象答的。你内部的条例、你产品的说明书,它一个字都没读过。

三、今天目标

30—60 分钟。做完你手上会有一个能读你自己 PDF 的助理。

你问它文件里的一句话,它给你答案,并说出出自哪个文件、第几页。

随便一个聊天框都能"看起来"回答文档问题;

值钱的是它能指给你看原文在哪,你才分得清它是在答还是在编。

四、前置检查

1. 一台还能跑的小喵助理(Day1 建的,别新建)

2. 它已经会调工具(Day3 做完,终端里见过 🔧 调用 ... 那一行)

3. Python 3.10 以上,配好 PyCharm

4. 一份你自己的 PDF——条例、说明书、论文、产品手册都行,20—80 页最合适

Day1 装依赖时已带了 pypdf>=4.2.0,不用重装;Key 照旧写在 .env 里,不要写死在代码里。

先确认那份 PDF 能不能选中文字:选一行按 Ctrl+C,复制不出来就是扫描件(见 Q1)。

五、概念:拆 → 找 → 答,只有三步

今天不碰底层原理,你不需要听懂"向量"也能跑通。

• 拆:把 PDF 变成一段一段的文字,每段都记住"我来自哪个文件、第几页"。

• 找:你一提问,它就按关键词给每段打分,挑出最相关的几段。

• 答:只把这几段原文塞给大模型,要它在答案里标明出处。

记住关键一句:它不是把文件"背"下来了,而是每次你提问,它都去翻一遍。

六、动手:四步,让它读上你的文件

第 1 步:把 PDF 复制进 data/ 目录

进到 xiaomiao-agent/,里面有个 data/ 目录(没有就自己建,全小写),把 PDF 复制进去。

文件名请起成人话:公文处理条例.pdf 比 扫描件001.pdf 强太多——

答案里那个"出自哪个文件",用的就是这个文件名。

这个目录是在 config.py 里定义好的,打开确认一眼:

DATA_DIR = BASE_DIR / "data"            # 你自己的资料(PDF 放这里)

第 2 步:新建 rag.py

在工程根目录新建 rag.py,把下面这段抄进去(以下是 rag.py 的全文):

# -*- coding: utf-8 -*-"""rag.py —— Day5:让它读你自己的 PDF,并且指着原文回答。三步:拆(把 PDF 变成一段段文字)→ 找(按关键词找出最相关的几段)→ 答(只把这几段塞给模型,并要求它标出处)。这里刻意不引入向量数据库:先用纯 Python 的关键词打分跑通,你已经能听懂"检索增强"到底在增强什么,再决定要不要升级。"""import mathimport refrom pypdf import PdfReaderimport configCHUNK_SIZE = 400      # 每段目标字数OVERLAP = 80          # 相邻段的重叠,避免答案正好被切断def load_pdf(path) -> list[dict]:    """把 PDF 拆成 [{来源, 页码, 内容}, ...]。"""    chunks = []    reader = PdfReader(str(path))    for page_no, page in enumerate(reader.pages, start=1):        text = (page.extract_text() or "").replace("\n", "")        text = re.sub(r"\s+", " ", text).strip()        if not text:            continue                                # 扫描件(图片型 PDF)抽不出字,跳过        step = CHUNK_SIZE - OVERLAP        for i in range(0, len(text), step):            piece = text[i:i + CHUNK_SIZE]            if len(piece) > 50:                chunks.append({                    "source": f"{path.name} 第{page_no}页",                    "content": piece,                })    return chunksdef _terms(query: str) -> list[str]:    """提问切词:中文按 2 字一段滑窗,英文按单词。够土,但对中文管用。"""    cn = re.findall(r"[\u4e00-\u9fff]{2,}", query)    words = re.findall(r"[A-Za-z0-9]{3,}", query)    grams = [s[i:i + 2] for s in cn for i in range(len(s) - 1)]    return list(dict.fromkeys(grams + [w.lower() for w in words]))def search(query: str, chunks: list[dict], top_k: int = 4) -> list[dict]:    """关键词打分(TF-IDF 的简化版):命中的词越稀有、出现越多,得分越高。"""    terms = _terms(query)    df = {t: sum(1 for c in chunks if t in c["content"]) for t in terms}    scored = []    for c in chunks:        score = 0.0        for t in terms:            tf = c["content"].count(t)            if tf and df[t]:                score += tf * math.log(len(chunks) / df[t])        if score > 0:            scored.append({**c, "score": round(score, 2)})    scored.sort(key=lambda x: -x["score"])    return scored[:top_k]def build_context(hits: list[dict]) -> str:    """把命中的段落拼成给模型看的资料块,带上出处。"""    if not hits:        return "(资料库里没有找到和问题相关的内容)"    return "\n\n".join(f"[{h['source']}]\n{h['content']}" for h in hits)def load_library() -> list[dict]:    """把 data/ 目录下所有 PDF 装进资料库。"""    chunks = []    for pdf in sorted(config.DATA_DIR.glob("*.pdf")):        chunks += load_pdf(pdf)    return chunks

三个地方先讲清。

① OVERLAP = 80 为什么要重叠? 一段话正好被从中间切断,答案那句就断了;

留 80 字重叠,保证关键句完整落进至少一段。

② _terms() 的中文切词为什么这么土? 中文没空格,这里是按 2 个字往前滑一格:

"公文种类"会被切成"公文""文种""种类"三个词。土,但对中文管用。

③ 打分是 TF-IDF 的简化版:一个词在资料里越稀有,命中它的段落得分越高。

"的""了"满篇都是、几乎不加分;"公文"只出现在几段里,很值钱。

第 3 步:改 tools.py,加三处

第一处,一个 _LIBRARY 缓存:资料库只从硬盘装一次,之后都在内存里查。

# ── Day5 新增:翻你自己的资料 ──────────────────────────_LIBRARY: list = []      # 资料库只装一次,之后每次提问都在里面查

第二处,search_docs() 函数:import rag 写在函数里,这叫延迟导入——

rag.py 要 import config,写在文件顶部容易绕成循环。

def search_docs(query: str) -> str:    """在 data/ 里你自己的 PDF 中检索,返回带出处的原文段落。"""    import rag                                     # 延迟导入,避免循环依赖    global _LIBRARY    if not _LIBRARY:        _LIBRARY = rag.load_library()    if not _LIBRARY:        return "资料库是空的:请先把 PDF 放进 data/ 目录。"    hits = rag.search(query, _LIBRARY, top_k=4)    return rag.build_context(hits)

第三处,TOOLS 列表里那份说明书——今天最重要的一段文字,原样抄:

    {        "type": "function",        "function": {            "name": "search_docs",            "description": (                "在主人自己的资料库(data 目录里的 PDF)中检索原文。"                "凡是涉及资料原文、文件内容、条例规定的问题,必须先用它查,"                "再根据返回的原文回答,并在答案里标明出自哪个文件哪一页。"                "没查到就直说没查到,不要凭常识编。"            ),            "parameters": {                "type": "object",                "properties": {"query": {"type": "string", "description": "要检索的关键词或问题"}},                "required": ["query"],            },        },    },

再在 TOOL_MAP 里加一行,把函数名和函数对上:

    "search_docs": search_docs,          # ← Day5 加的这一行

多说一句:函数写出来了,不等于它会去用——模型只看得懂说明书,所以这段文字写死了

三件事:涉及原文必须先用它查、答案里要标明出自哪个文件哪一页、没查到就直说别编。

模型的行为,是被这段文字约束出来的。

第 4 步:跑起来验证

在 Terminal 里跑 python main.py,等它打印出 你 >,敲:

你 > 资料里对公文种类是怎么规定的

它会先打印一行工具调用,形如:

  🔧 调用 search_docs({"query": "公文种类"}) → [公文处理条例.pdf 第1页] ……

箭头后面省略的一大串,就是它翻出来的原文(带 [文件名 第X页] 标头)。

留意:资料库只在你第一次提问时装载一次(那个 _LIBRARY 缓存),所以第一次会顿一下,

之后提问都很快。

再追问一句"这条出自哪一页,原文怎么写的",能引出原句,今天就成了。

🔴 隐私红线:动手之前,先读这一段

今天你要把自己电脑里的文件交给云端的接口去处理,这一段不能跳。

不要放这些资料进 data/:公司机密、内部经营数据、客户名单与联系方式;

身份证、护照等证件,病历与体检报告;合同原件与报价单。

调用云端接口,意味着文件内容会离开你的电脑,被发送到大模型的服务器上。

涉密材料请用本地模型跑,或者干脆不用。

涉及别人的信息先脱敏:真名换成"张先生""甲公司",手机号、身份证号先删掉。

七、卡住了怎么办

Q1:它回"资料库是空的",或者什么都不返回。

八成是扫描件:手机拍照的 PDF 本质是一张张图片,里面没有一个字,

page.extract_text() 对它就返回空,load_pdf() 里那句会把它跳过。

确认方法:选一行按 Ctrl+C,复制不了就是图片;解法是先做 OCR 转成文字版。

Q2:ModuleNotFoundError: No module named 'pypdf'

包没装进当前解释器。在 Terminal 里敲:

pip install -r requirements.txt

敲完确认 pip -V 的路径在 .venv 里;不在就重装一次,

PyCharm 右下角的解释器也要选成同一个环境。

Q3:答"资料库里没有找到和问题相关的内容"。

三个办法按顺序试:换词,用资料里真实出现过的词提问;

把问题写具体,别问"这份资料讲了什么",问"第 2 条对退款时限是怎么规定的";

调大 top_k,把 search_docs() 里的 top_k=4 改成 8,代价是发给模型的文字变多。

Q4:它还是编,答得头头是道但原文里没有。

先改工具说明书(第 3 步第三处),把"没查到就直说,不要凭常识编"写得更死,

再当场追问:这条出自哪一页,原文怎么写的?如果原文里没有,请直接说没有。

它一引原文就现形;引不出来,就是它在编。

Q5:几十页的 PDF,第一次提问卡住好几秒。

正常的。_LIBRARY 缓存就是这么设计的:第一次装载,后面全快。

别以为卡死就 Ctrl+C——中断了下次还得从头装。

Q6:PDF 里全是表格或双栏,抽出来的文字顺序是乱的。

page.extract_text() 按阅读顺序猜,遇表格和双栏就会串行,不是你的代码写错了。

解法:换排版干净的原文,或先转成纯文本再放进去。

八、今日代码清单

四条:

① 补装依赖(确认 pypdf 在)

pip install -r requirements.txt

② 把 PDF 复制进 data/(资源管理器里复制粘贴即可,无命令)

xiaomiao-agent/data/公文处理条例.pdf

③ 跑起来

python main.py

④ 敲进去的第一个问题

资料里对公文种类是怎么规定的

九、支线加餐【进阶,可跳过】

今天 rag.py 的打分是"关键词对不对得上",短板很明显:

同一个意思换个说法,它就找不到了。 你问"怎么请长假",资料里写的是"年休假申请",

可能一分都不给。

升级方向叫向量检索,思路只有一句话:先把每段文字变成一串数字,

意思相近的两段,数字就长得像;提问时把问题也变成一串数字,再去比"像不像",

挑最像的几段——这样"请假"和"休假"会被认成同一件事。

要改的位置很小:rag.py 里把 search() 的打分方式换掉就行,其它三处不用动。

def search(query: str, chunks: list[dict], top_k: int = 4) -> list[dict]:    """向量版:把 query 和每个 chunk 都变成一串数字,按"像不像"排序。"""    # 1) 调一个"把文字变成数字"的接口,拿到 query 那串数字    # 2) 每一段也做一次(或者提前算好存起来,别每次都算)    # 3) 按两组数字的相似程度从大到小排序,取前 top_k 段    ...

今天先不要写这段代码,它多一个依赖、多一份花费;撞上这堵墙再动手不迟。

十、作业与验收

作业:把你自己的资料放进 data/,问 3 个"只有原文才知道"的冷门问题——

在网上查不到答案的那种,比如某个具体数字、某个条款的编号:

• 这份条例一共规定了几种公文种类,第 3 种叫什么?

• 退款申请最迟要在几个工作日内处理完?

验收标准(硬):3 题至少答对 2 个,并且能说出出自哪个文件、第几页。

再补一个动作:从它给的出处里挑一条,翻到你那份 PDF 的那一页对一眼。

对得上,这个资料库就是真能用的。

三档进度:保底是 PDF 被读到、答对 1 题;标准是 3 题答对 2 题、出处能核对;

进阶是把第九段看懂了。

十一、今日互动 + 明日预告

今天想看你留下「资料类型 + 它答对的那一题」;被它编过一次的,更值得发出来。

回复【Day5】获取今天的完整代码与配置模板。

卡住了?评论区留「Day5 + 报错原文」,我一条条回。

明天:把它装进微信——用 Flask 起一个服务,别人在微信里就能跟它说话。

相关学习资料