从零搭一个会查资料的文档问答助手,Python源码已附
AI工具 实战案例分享|附源码
在过去两年里,AI 工具已经从“尝鲜型产品”逐渐变成了很多团队的“生产力基础设施”。无论是内容创作、客服问答、代码辅助、数据分析,还是企业知识库检索,AI 都正在改变工作流。
但在实际落地过程中,很多人会遇到一个问题:AI 看起来很强,但不知道如何把它真正用到业务场景里。
本文将通过一个完整的实战案例,分享如何使用 AI 工具搭建一个“智能文档问答助手”。这个助手可以读取本地文档内容,用户输入问题后,系统会基于文档内容进行回答,而不是单纯依赖大模型的通用知识。
本文包含:
- 项目背景与适用场景
- 技术实现思路
- 核心流程拆解
- 完整 Python 示例源码
- 运行方式
- 可扩展方向与落地建议
一、案例背景:为什么要做智能文档问答助手?
在企业或个人工作中,我们经常会面对大量文档,例如:
- 产品说明书
- 项目需求文档
- 公司制度文件
- 技术方案文档
- 培训资料
- FAQ 文档
- 合同条款
- 会议纪要
传统方式下,如果我们想从几十页甚至上百页文档里找到某个答案,通常需要手动搜索关键词,再逐段阅读。这个过程不仅效率低,而且容易遗漏上下文信息。
例如,一个员工可能会问:
“公司报销差旅费需要提供哪些材料?”
如果公司有一份《财务报销制度.pdf》,那么理论上答案就在文档里。但如果员工不熟悉文档结构,就很难快速定位。
这时候,我们可以借助 AI 构建一个文档问答系统:
- 先读取文档内容;
- 将文档切分成多个小片段;
- 根据用户问题,从文档中检索最相关的片段;
- 把相关片段和问题一起发送给大模型;
- 让大模型基于文档内容生成答案。
这种方案通常被称为 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 格式略有差异,但整体思路类似:
- 申请 API Key;
- 配置环境变量;
- 修改
LLM_API_URL和LLM_MODEL; - 根据平台返回格式调整
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 就可以成为稳定可靠的生产力工具。