上一篇 下一篇 分享链接 返回 返回顶部

从零搭一个会查资料的文档问答助手,Python源码已附

发布人:慈云数据-客服中心 发布时间:7小时前 阅读量:2

AI工具 实战案例分享|附源码

在过去两年里,AI 工具已经从“尝鲜型产品”逐渐变成了很多团队的“生产力基础设施”。无论是内容创作、客服问答、代码辅助、数据分析,还是企业知识库检索,AI 都正在改变工作流。

但在实际落地过程中,很多人会遇到一个问题:AI 看起来很强,但不知道如何把它真正用到业务场景里。

本文将通过一个完整的实战案例,分享如何使用 AI 工具搭建一个“智能文档问答助手”。这个助手可以读取本地文档内容,用户输入问题后,系统会基于文档内容进行回答,而不是单纯依赖大模型的通用知识。

本文包含:

  • 项目背景与适用场景
  • 技术实现思路
  • 核心流程拆解
  • 完整 Python 示例源码
  • 运行方式
  • 可扩展方向与落地建议

一、案例背景:为什么要做智能文档问答助手?

在企业或个人工作中,我们经常会面对大量文档,例如:

  • 产品说明书
  • 项目需求文档
  • 公司制度文件
  • 技术方案文档
  • 培训资料
  • FAQ 文档
  • 合同条款
  • 会议纪要

传统方式下,如果我们想从几十页甚至上百页文档里找到某个答案,通常需要手动搜索关键词,再逐段阅读。这个过程不仅效率低,而且容易遗漏上下文信息。

例如,一个员工可能会问:

“公司报销差旅费需要提供哪些材料?”

如果公司有一份《财务报销制度.pdf》,那么理论上答案就在文档里。但如果员工不熟悉文档结构,就很难快速定位。

这时候,我们可以借助 AI 构建一个文档问答系统:

  1. 先读取文档内容;
  2. 将文档切分成多个小片段;
  3. 根据用户问题,从文档中检索最相关的片段;
  4. 把相关片段和问题一起发送给大模型;
  5. 让大模型基于文档内容生成答案。

这种方案通常被称为 RAG,即 Retrieval-Augmented Generation,中文可以理解为“检索增强生成”。

它的核心价值是:

不让大模型凭空回答,而是让它基于指定资料回答。

这样可以有效减少“幻觉”,提升答案的准确性和可控性。


二、项目目标

本案例要实现一个简化版的智能文档问答助手,具备以下能力:

  • 读取本地 .txt 文档;
  • 自动切分文档内容;
  • 根据用户问题检索相关文本片段;
  • 调用大模型生成回答;
  • 在命令行中进行交互式问答;
  • 输出参考片段,方便用户核对答案来源。

为了便于学习和二次开发,本文选择 Python 实现,并尽量减少复杂依赖。


三、技术方案设计

整个系统可以分为五个模块:

用户问题
   ↓
文档读取
   ↓
文本切分
   ↓
相关内容检索
   ↓
大模型生成答案
   ↓
返回结果

1. 文档读取

本案例以 .txt 文件为例,原因是文本格式简单,适合演示核心逻辑。如果要支持 PDF、Word、网页等格式,可以后续接入对应解析库。

2. 文本切分

大模型通常有上下文长度限制,不能一次性把整本文档都塞进去。因此需要先把长文档切成多个小块。

例如每 500 个字符作为一个片段,片段之间可以设置一定重叠,避免上下文被切断。

3. 相关内容检索

为了降低依赖,本案例采用一种简单的关键词匹配方案:

  • 对用户问题进行分词或字符匹配;
  • 计算问题和文档片段之间的相似度;
  • 选出得分最高的若干片段。

在生产环境中,可以使用 Embedding 向量检索,例如:

  • OpenAI Embeddings
  • text-embedding 模型
  • Sentence Transformers
  • FAISS
  • Milvus
  • pgvector
  • Elasticsearch

但为了让代码更容易运行,本文先采用轻量级实现。

4. 大模型生成答案

检索到相关片段后,将它们作为上下文,与用户问题一起构造 Prompt,然后发送给 AI 模型。

Prompt 设计非常关键。我们希望模型:

  • 只能基于提供的文档内容回答;
  • 如果文档中没有答案,就明确说明“不确定”;
  • 不要编造信息;
  • 答案尽量结构化。

5. 返回答案和参考片段

为了方便用户核对,我们不仅返回 AI 生成的回答,还会返回本次使用的参考文本片段。

这在实际业务中非常重要,因为很多企业场景都要求“可追溯”。


四、项目目录结构

建议创建如下项目结构:

ai-doc-qa/
├── app.py
├── docs/
│   └── sample.txt
└── requirements.txt

其中:

  • app.py:主程序源码;
  • docs/sample.txt:示例文档;
  • requirements.txt:依赖列表。

五、准备示例文档

docs/sample.txt 中写入以下内容:

公司差旅报销制度

一、适用范围
本制度适用于公司全体正式员工、试用期员工以及经公司批准参与出差的外部合作人员。

二、报销原则
员工因公出差产生的交通费、住宿费、餐饮补贴及其他合理费用,可以按照公司规定申请报销。所有报销项目必须真实、合理,并与实际出差任务相关。

三、交通费报销
员工出差可以选择高铁、飞机、长途汽车等交通方式。普通员工原则上乘坐二等座或经济舱。部门负责人及以上级别员工可以根据实际情况申请一等座或公务舱,但需提前获得审批。

四、住宿费标准
一线城市住宿费标准为每晚不超过600元;二线城市住宿费标准为每晚不超过450元;其他城市住宿费标准为每晚不超过350元。超出标准部分原则上不予报销,特殊情况需提交说明并获得部门负责人审批。

五、餐饮补贴
员工出差期间可以享受餐饮补贴。国内出差餐饮补贴标准为每天100元,海外出差餐饮补贴标准根据目的地国家和地区另行规定。

六、报销材料
员工提交差旅报销申请时,需要提供以下材料:出差审批单、交通票据、住宿发票、费用明细清单。如涉及客户拜访、会议参会等事项,还需提供相关证明材料。

七、报销流程
员工应在出差结束后15个工作日内提交报销申请。申请提交后,由直属上级进行初审,财务部门进行复核,最终由有权审批人完成审批。审批通过后,财务部门将在5个工作日内安排付款。

八、违规处理
如员工提交虚假票据、虚构出差事项或重复报销,公司有权拒绝报销,并根据公司相关纪律规定进行处理。情节严重的,公司可以解除劳动合同。

六、安装依赖

本案例使用 Python 标准库即可完成基础检索逻辑。为了便于调用大模型接口,这里使用 requests

requirements.txt 内容如下:

requests==2.31.0

安装依赖:

pip install -r requirements.txt

七、完整源码

下面是 app.py 的完整代码。

注意:代码中的大模型 API 调用部分使用通用 HTTP 接口示例。你可以根据自己使用的平台进行替换,例如 OpenAI、通义千问、智谱、DeepSeek、Moonshot、百度千帆等。

import os
import re
import json
import requests
from typing import List, Dict, Tuple


# =========================
# 基础配置
# =========================

DOC_PATH = "docs/sample.txt"

# 这里填写你的大模型 API 地址
# 不同平台接口格式可能不同,需要根据实际文档调整
LLM_API_URL = os.getenv("LLM_API_URL", "https://api.example.com/v1/chat/completions")

# 这里填写你的 API Key
LLM_API_KEY = os.getenv("LLM_API_KEY", "YOUR_API_KEY")

# 模型名称,根据你所使用的平台填写
LLM_MODEL = os.getenv("LLM_MODEL", "your-model-name")


# =========================
# 文档读取
# =========================

def load_document(path: str) -> str:
    """
    读取本地 txt 文档内容
    """
    if not os.path.exists(path):
        raise FileNotFoundError(f"文档不存在:{path}")

    with open(path, "r", encoding="utf-8") as f:
        return f.read()


# =========================
# 文本切分
# =========================

def split_text(text: str, chunk_size: int = 300, overlap: int = 50) -> List[str]:
    """
    将长文本切分成多个片段

    参数:
    text: 原始文本
    chunk_size: 每个片段最大字符数
    overlap: 相邻片段之间的重叠字符数
    """
    text = re.sub(r"\s+", "\n", text).strip()

    chunks = []
    start = 0
    text_length = len(text)

    while start < text_length:
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)

        if end >= text_length:
            break

        start = end - overlap

    return chunks


# =========================
# 简单分词与相似度计算
# =========================

def tokenize(text: str) -> List[str]:
    """
    简单中文分词函数

    为了减少依赖,这里使用一种朴素方式:
    1. 提取中文、英文、数字;
    2. 中文按连续字符和单字混合处理;
    3. 英文转小写。

    生产环境建议使用 jieba、HanLP 或向量 Embedding。
    """
    text = text.lower()
    tokens = []

    # 提取英文和数字词
    words = re.findall(r"[a-zA-Z0-9]+", text)
    tokens.extend(words)

    # 提取中文字符
    chinese_chars = re.findall(r"[\u4e00-\u9fff]", text)
    tokens.extend(chinese_chars)

    # 提取常见中文短语,增强匹配效果
    common_terms = [
        "报销", "差旅", "交通费", "住宿费", "餐饮补贴",
        "审批", "发票", "材料", "流程", "违规",
        "出差", "工作日", "一线城市", "二线城市"
    ]

    for term in common_terms:
        if term in text:
            tokens.append(term)

    return tokens


def calculate_score(question: str, chunk: str) -> float:
    """
    计算用户问题和文档片段之间的简单相关性得分
    """
    question_tokens = tokenize(question)
    chunk_tokens = tokenize(chunk)

    if not question_tokens or not chunk_tokens:
        return 0.0

    question_set = set(question_tokens)
    chunk_set = set(chunk_tokens)

    common = question_set.intersection(chunk_set)

    # 基础得分:共同词数量
    score = len(common)

    # 如果片段中包含完整问题里的关键词,给予额外加权
    for token in question_set:
        if len(token) >= 2 and token in chunk:
            score += 2

    return float(score)


def retrieve_relevant_chunks(question: str, chunks: List[str], top_k: int = 3) -> List[Tuple[str, float]]:
    """
    从文档片段中检索与问题最相关的 top_k 个片段
    """
    scored_chunks = []

    for chunk in chunks:
        score = calculate_score(question, chunk)
        scored_chunks.append((chunk, score))

    scored_chunks.sort(key=lambda x: x[1], reverse=True)

    # 过滤掉得分为 0 的片段
    results = [(chunk, score) for chunk, score in scored_chunks if score > 0]

    return results[:top_k]


# =========================
# Prompt 构造
# =========================

def build_prompt(question: str, contexts: List[str]) -> str:
    """
    构造发送给大模型的 Prompt
    """
    context_text = "\n\n".join(
        [f"【参考片段{i + 1}】\n{ctx}" for i, ctx in enumerate(contexts)]
    )

    prompt = f"""
你是一个严谨的企业文档问答助手。请你只根据下面提供的参考资料回答用户问题。

回答要求:
1. 如果参考资料中能找到答案,请准确回答;
2. 如果参考资料中没有明确答案,请回答“根据现有资料无法确定”;
3. 不要编造不存在的信息;
4. 回答尽量条理清晰;
5. 如涉及流程、材料、标准,请使用列表形式展示。

参考资料:
{context_text}

用户问题:
{question}

请给出答案:
""".strip()

    return prompt


# =========================
# 调用大模型
# =========================

def call_llm(prompt: str) -> str:
    """
    调用大模型接口

    说明:
    这里使用 OpenAI Chat Completions 风格的接口格式。
    如果你使用其他平台,请根据对应接口文档调整 payload 和解析逻辑。
    """
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {LLM_API_KEY}"
    }

    payload = {
        "model": LLM_MODEL,
        "messages": [
            {
                "role": "system",
                "content": "你是一个专业、严谨、不会编造信息的中文 AI 助手。"
            },
            {
                "role": "user",
                "content": prompt
            }
        ],
        "temperature": 0.2
    }

    try:
        response = requests.post(
            LLM_API_URL,
            headers=headers,
            data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
            timeout=60
        )

        response.raise_for_status()
        data = response.json()

        # OpenAI 风格返回解析
        return data["choices"][0]["message"]["content"].strip()

    except Exception as e:
        return f"调用大模型失败:{str(e)}"


# =========================
# 本地兜底回答
# =========================

def local_fallback_answer(question: str, contexts: List[str]) -> str:
    """
    当没有配置大模型接口时,提供一个本地兜底答案。
    这个函数不会真正理解问题,只是把相关片段返回。
    """
    if not contexts:
        return "根据现有资料无法确定。"

    joined_context = "\n\n".join(contexts)

    return (
        "当前未成功调用大模型,以下是根据关键词检索到的相关资料,"
        "请人工核对后使用:\n\n"
        f"{joined_context}"
    )


# =========================
# 主问答流程
# =========================

def answer_question(question: str, chunks: List[str]) -> Dict:
    """
    完整问答流程:
    1. 检索相关片段
    2. 构造 Prompt
    3. 调用大模型生成答案
    4. 返回答案和参考来源
    """
    retrieved = retrieve_relevant_chunks(question, chunks, top_k=3)
    contexts = [item[0] for item in retrieved]

    if not contexts:
        return {
            "answer": "根据现有资料无法确定。",
            "references": []
        }

    prompt = build_prompt(question, contexts)

    if LLM_API_KEY == "YOUR_API_KEY":
        answer = local_fallback_answer(question, contexts)
    else:
        answer = call_llm(prompt)

    return {
        "answer": answer,
        "references": [
            {
                "content": chunk,
                "score": score
            }
            for chunk, score in retrieved
        ]
    }


def main():
    print("正在加载文档...")
    document = load_document(DOC_PATH)

    print("正在切分文档...")
    chunks = split_text(document, chunk_size=300, overlap=50)

    print(f"文档加载完成,共切分为 {len(chunks)} 个片段。")
    print("你可以开始提问,输入 exit 退出。")
    print("-" * 60)

    while True:
        question = input("\n请输入你的问题:").strip()

        if question.lower() in ["exit", "quit", "q"]:
            print("已退出。")
            break

        if not question:
            print("问题不能为空,请重新输入。")
            continue

        result = answer_question(question, chunks)

        print("\nAI 回答:")
        print(result["answer"])

        print("\n参考片段:")
        if not result["references"]:
            print("无")
        else:
            for i, ref in enumerate(result["references"], start=1):
                print(f"\n【参考片段 {i}|相关性得分:{ref['score']}】")
                print(ref["content"])

        print("-" * 60)


if __name__ == "__main__":
    main()

八、运行项目

在项目根目录执行:

python app.py

运行后,可以输入问题,例如:

差旅报销需要提供哪些材料?

如果你还没有配置大模型接口,程序会使用本地兜底逻辑,返回相关文档片段。

示例输出可能如下:

AI 回答:
当前未成功调用大模型,以下是根据关键词检索到的相关资料,请人工核对后使用:

六、报销材料
员工提交差旅报销申请时,需要提供以下材料:出差审批单、交通票据、住宿发票、费用明细清单。如涉及客户拜访、会议参会等事项,还需提供相关证明材料。

如果你配置了真实大模型 API,回答可能会更自然:

AI 回答:
根据资料,员工提交差旅报销申请时,需要提供以下材料:

1. 出差审批单;
2. 交通票据;
3. 住宿发票;
4. 费用明细清单;
5. 如涉及客户拜访、会议参会等事项,还需提供相关证明材料。

九、如何接入真实大模型 API?

不同平台的 API 格式略有差异,但整体思路类似:

  1. 申请 API Key;
  2. 配置环境变量;
  3. 修改 LLM_API_URLLLM_MODEL
  4. 根据平台返回格式调整 call_llm 函数。

例如在 Linux 或 macOS 中可以这样配置:

export LLM_API_KEY="你的API Key"
export LLM_API_URL="https://你的模型服务地址/v1/chat/completions"
export LLM_MODEL="你的模型名称"

Windows PowerShell 中可以这样配置:

$env:LLM_API_KEY="你的API Key"
$env:LLM_API_URL="https://你的模型服务地址/v1/chat/completions"
$env:LLM_MODEL="你的模型名称"

如果你使用的是兼容 OpenAI 接口格式的平台,通常只需要修改这三个配置即可。


十、案例核心价值分析

这个案例虽然代码不复杂,但已经包含了很多 AI 应用落地的关键思路。

1. AI 不只是聊天,而是嵌入业务流程

很多人使用 AI 时,只是打开网页聊天窗口,然后复制粘贴内容。但真正有价值的 AI 应用,往往是把模型能力嵌入现有业务流程。

比如:

  • 客服系统中自动回答常见问题;
  • OA 系统中查询公司制度;
  • CRM 系统中总结客户跟进记录;
  • 项目管理系统中自动生成周报;
  • 知识库系统中进行语义问答。

2. RAG 是企业 AI 应用的重要基础

企业内部数据通常不会直接存在于大模型训练数据中,而且很多信息还具有时效性和私密性。

如果只依赖通用大模型,容易出现以下问题:

  • 回答过时;
  • 回答不符合企业制度;
  • 编造不存在的内容;
  • 无法引用来源;
  • 难以满足审计要求。

通过 RAG,可以让模型基于企业自己的资料进行回答,从而提高可靠性。

3. 检索质量决定回答上限

在 RAG 系统中,大模型只是最后一步。真正决定答案质量的,往往是检索环节。

如果检索出来的内容不相关,即使模型再强,也很难生成正确答案。

本案例使用的是简单关键词匹配,适合学习和小规模场景。但在生产环境中,建议升级为向量检索,例如:

用户问题 → 生成问题向量 → 在向量数据库中搜索相似文档块 → 返回相关上下文 → 大模型回答

这种方式可以理解语义,而不仅仅匹配关键词。

例如用户问:

“出差回来多久内要申请费用?”

文档中写的是:

“员工应在出差结束后15个工作日内提交报销申请。”

关键词可能没有完全一致,但语义高度相关。向量检索就更容易找到正确片段。


十一、可扩展优化方向

如果你想把这个 Demo 升级成一个真正可用的 AI 工具,可以从以下几个方向优化。

1. 支持更多文档格式

当前只支持 .txt,实际业务中更多是 PDF、Word、Excel、网页等格式。

可以引入:

pip install pymupdf python-docx pandas beautifulsoup4

对应能力包括:

  • 使用 PyMuPDF 解析 PDF;
  • 使用 python-docx 解析 Word;
  • 使用 pandas 解析 Excel;
  • 使用 BeautifulSoup 解析 HTML 页面。

2. 使用向量数据库

可以使用 FAISS 做本地向量检索,也可以使用 Milvus、Qdrant、Weaviate、pgvector 等服务。

典型流程如下:

文档 → 切分 → 生成 Embedding → 存入向量库
问题 → 生成 Embedding → 向量检索 → 拼接上下文 → 调用大模型

3. 增加引用来源

在切分文档时,可以为每个片段添加元信息:

  • 文件名;
  • 页码;
  • 章节标题;
  • 创建时间;
  • 文档版本。

这样模型回答时可以展示:

参考来源:《财务报销制度》第六章 报销材料

这对于企业内部系统非常重要。

4. 增加权限控制

企业知识库通常涉及权限问题。不同用户能看到的文档不同,因此检索时必须根据用户身份过滤数据。

例如:

  • 普通员工只能查看公开制度;
  • 部门经理可以查看部门资料;
  • 财务人员可以查看财务文档;
  • 管理层可以查看经营分析材料。

5. 增加答案审核机制

对于高风险场景,AI 答案不应该直接作为最终结论。可以增加人工审核或置信度判断。

例如:

  • 如果检索分数过低,提示用户“资料不足”;
  • 如果问题涉及法律、财务、人事等敏感事项,提示“请咨询相关部门”;
  • 如果答案来源不足,不允许模型自由发挥。

十二、实战经验总结

通过这个案例,可以总结出几个 AI 工具落地经验。

1. 先从小场景开始

不要一开始就做“大而全”的 AI 平台。更好的方式是选择一个高频、明确、边界清晰的问题。

例如:

  • 查询制度;
  • 生成日报;
  • 总结会议纪要;
  • 分类客服工单;
  • 提取合同关键信息。

小场景更容易验证价值,也更容易持续迭代。

2. 数据质量比模型选择更重要

很多团队在一开始就纠结“哪个模型最好”,但实际落地时,文档质量、数据结构、更新机制往往更重要。

如果知识库文档本身混乱、重复、过期,即使使用最强模型,也很难得到稳定结果。

3. Prompt 要写规则,而不是只写问题

好的 Prompt 应该告诉模型:

  • 它扮演什么角色;
  • 可以依据哪些资料;
  • 不知道时怎么回答;
  • 输出格式是什么;
  • 哪些内容不能编造。

本文中的 Prompt 就加入了“只根据参考资料回答”“无法确定时明确说明”等约束。

4. AI 系统要可追溯

尤其在企业场景中,用户不仅关心答案是什么,还关心答案来自哪里。

因此建议始终保留参考来源,让用户能够回到原文核对。

5. 不要完全依赖模型判断

AI 适合提高效率,但不应替代所有判断。对于重要决策,仍然需要人工复核。


十三、结语

本文通过一个“智能文档问答助手”的案例,展示了 AI 工具在真实业务中的一种常见落地方式。虽然代码示例相对简单,但已经覆盖了 AI 应用开发中的核心链路:

  • 文档读取;
  • 文本切分;
  • 内容检索;
  • Prompt 构造;
  • 大模型调用;
  • 答案生成;
  • 来源展示。

如果你是 AI 工具的初学者,可以先基于本文源码跑通 Demo,再逐步加入 PDF 解析、向量检索、Web 页面、用户权限等功能。

如果你是企业开发者,可以把这个思路扩展到内部知识库、客服系统、培训平台、制度查询系统等场景中。

AI 工具真正的价值,不在于“模型有多酷”,而在于它能不能解决一个具体、真实、重复发生的问题。只要选对场景,并用工程化方式持续优化,AI 就可以成为稳定可靠的生产力工具。

目录结构
全文