☰
基于RAG的智能审计问答系统:Python实战与优化指南
2026/10/11 11:38:23 网站建设 项目流程

简介:这份资源是面向高校学生与开发者的智能审计问答系统完整项目,基于大语言模型构建,适合用作毕业设计、期末大作业或课程设计的高分参考。项目聚焦审计领域的智能问答场景,将大模型能力与审计业务知识结合,帮助读者理解从数据组织到前端交互的完整实现路径,对Python与Web开发有一定基础的学习者尤为友好。压缩包共34个文件,约16.77MB,包含4个Python源码文件、5个HTML页面、3个CSS样式与2个JavaScript脚本,另有图片素材、Markdown说明文档、CSV数据文件及依赖配置等,覆盖后端逻辑、前端界面与项目文档各环节。目前已有684人学习关注。代码注释较为完整,新手也能看懂,下载后简单部署即可运行,可作为毕设或课程设计的直接参考,也便于在此基础上扩展审计问答功能与界面优化。

1. 智能审计问答系统:从凭证到结论,大模型到底能替你做哪几步

审计现场最耗时的环节往往不是查账本身,而是把散落在制度文件、历史底稿、凭证摘要里的信息拼成一条能站住脚的结论。一个基于大语言模型的智能审计问答系统,核心目标就是让审计人员用自然语言提问,系统从私有知识库中检索依据、组织答案、标注出处,而不是让模型凭空编造。它适合两类人:一类是手上有大量审计文档、想用 RAG 做内部工具的开发者;另一类是想理解大模型在垂直领域怎么落地、需要一套可复现 Python 方案的工程师。这套方案不碰通用聊天,只解决“问审计问题、给有依据的回答”这一件事。

2. 系统骨架怎么搭:文档解析、向量检索、答案生成三段式

2.1 为什么审计场景必须用 RAG 而不是直接微调

直接拿通用大模型问“这笔费用的列支依据是什么”,它大概率会给你一段听起来合理但完全不对应你单位制度的回答。审计结论要求可追溯,模型必须能指向具体文件、具体条款。RAG 的思路是先把审计相关文档切块、向量化、存进向量库,用户提问时先检索最相关的若干片段,再让模型基于这些片段生成答案。这样做的好处是知识更新只需重新入库,不用重新训练模型;坏处是检索质量直接决定答案上限,切块策略和嵌入模型选型成了整个系统最关键的参数。

常见做法是文档解析层用unstructured或pdfplumber处理 PDF、Word、Excel,把表格和正文分开处理。审计文档里表格占比高,费用明细、科目余额表如果直接按字符切,表头和数据行会被切散,检索时匹配到半张表反而误导模型。我一般会把表格单独转成 Markdown 再切块,正文按语义段落切,块大小控制在 500 到 800 字符,重叠 100 字符左右。嵌入模型选中文效果稳定的,比如bge-large-zh这类,不要用英文为主的模型硬扛中文审计术语。

2.2 最小可运行代码:从文档入库到一次问答

下面这段代码把文档加载、切块、向量化、检索、生成串成一条最小链路。依赖需要提前装好langchain、chromadb、sentence-transformers和任意一个兼容 OpenAI 接口的模型服务。

# audit_qa_min.py # 最小审计问答链路:加载文档 -> 切块 -> 向量入库 -> 检索 -> 生成 from langchain.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma from langchain.llms import OpenAI from langchain.chains import RetrievalQA # 1. 加载审计制度文档,实际项目可换成 PDF/Word 加载器 loader = TextLoader("./data/audit_policy.txt", encoding="utf-8") docs = loader.load() # 2. 切块:审计文档段落较长,块大小 600,重叠 100 防止条款被切断 splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ","] ) chunks = splitter.split_documents(docs) # 3. 嵌入模型:中文审计术语多,选中文语义模型 embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5") # 4. 向量库持久化到本地,避免每次重启重新嵌入 vectordb = Chroma.from_documents( documents=chunks, embedding=embedding, persist_directory="./chroma_audit" ) vectordb.persist() # 5. 检索器:返回最相关的 4 个片段,太少漏依据,太多干扰生成 retriever = vectordb.as_retriever(search_kwargs={"k": 4}) # 6. 生成链:temperature 设 0.1,审计答案要稳不要飘 qa = RetrievalQA.from_chain_type( llm=OpenAI(temperature=0.1), chain_type="stuff", retriever=retriever, return_source_documents=True ) # 7. 提问并打印答案和出处 result = qa({"query": "差旅费报销需要哪些附件?"}) print("答案:", result["result"]) for doc in result["source_documents"]: print("出处片段:", doc.page_content[:120])

这段代码里几个参数直接决定效果。chunk_size设 600 是因为审计条款通常两三句话一个完整意思,太大检索精度下降,太小依据不完整。k=4是经验值,实际可以调到 3 到 6 之间做对比。temperature=0.1是为了让模型尽量复述检索到的内容,而不是自由发挥。return_source_documents=True必须开,审计场景没有出处的答案等于没有答案。

2.3 检索质量差时先查这三个地方

检索不准,先别急着换模型。第一看切块,把切出来的块打印前二十个,如果发现大量块以半句话开头,说明分隔符没配好。第二看嵌入模型是否真的支持中文,有些模型英文强中文弱,审计术语匹配会明显偏差。第三看向量库距离度量,Chroma 默认 l2,中文语义检索通常 cosine 更稳,可以在创建 collection 时指定。这三步排查完,再考虑换更大的嵌入模型或加 rerank。

3. 把问答做成审计工具:多轮追问、出处标注与权限隔离

3.1 多轮追问怎么保留上下文又不串话题

审计人员不会只问一句就结束,常见的是“那住宿标准呢”“如果超标了怎么处理”。单轮 RAG 每次只拿当前问题去检索,第二轮问“那住宿标准呢”时,检索器不知道“那”指差旅费,召回结果会跑偏。解决办法是在检索前把历史对话压缩成独立问题。可以用一个轻量 prompt 让模型把“那住宿标准呢”改写成“差旅费报销的住宿标准是什么”,再拿改写后的问题去检索。这样既保留上下文,又不会把整段历史塞进检索 query 导致噪声。

# condense_question.py # 把多轮对话压缩成独立检索问题,避免指代导致召回跑偏 from langchain.chains import ConversationalRetrievalChain from langchain.llms import OpenAI qa = ConversationalRetrievalChain.from_llm( llm=OpenAI(temperature=0.1), retriever=retriever, return_source_documents=True, # 关键:开启问题改写,把“那住宿标准呢”补全成完整问题 condense_question_llm=OpenAI(temperature=0) ) chat_history = [] result = qa({"question": "差旅费报销需要哪些附件?", "chat_history": chat_history}) chat_history.append(("差旅费报销需要哪些附件?", result["answer"])) result2 = qa({"question": "那住宿标准呢?", "chat_history": chat_history}) print(result2["answer"])

condense_question_llm单独设 temperature 0,是因为改写只需要准确补全,不需要创造性。chat_history用元组列表维护,注意不要无限增长,超过十轮后旧对话对当前问题帮助很小,反而增加改写噪声,可以只保留最近五轮。

3.2 出处标注要做到条款级而不是文件级

只告诉用户“出自差旅费管理办法”不够,审计底稿需要精确到第几条。实现方式是在切块时把条款号写进 metadata,检索返回后把 metadata 一起展示。比如切块前用正则识别“第X条”,把它存进chunk.metadata["article"],前端展示时显示“出处:差旅费管理办法 第十二条”。这样审计人员能直接翻到原文核对,而不是在整份文件里大海捞针。

# metadata_article.py # 切块时提取条款号写入 metadata,检索结果可精确到条 import re from langchain.schema import Document def split_with_article(text, source_name): # 按“第X条”切分,保留条款号作为 metadata pattern = re.compile(r"(第[一二三四五六七八九十百\d]+条)") parts = pattern.split(text) docs = [] current_article = "未知" for part in parts: if pattern.fullmatch(part): current_article = part elif part.strip(): docs.append(Document( page_content=part.strip(), metadata={"source": source_name, "article": current_article} )) return docs

这个函数把文本按条款切开,每个块带上条款号。注意pattern.split会把分隔符也保留在结果里,所以用fullmatch判断当前片段是不是条款号。实际审计文件里条款格式可能不统一,有的用“第 12 条”带空格,正则要相应放宽。切完后建议人工抽查十条,确认条款号和内容对应没错位。

3.3 权限隔离:不同角色只能检索到授权文档

审计系统里不同项目组能看的底稿不同,向量库如果混在一起,检索时可能把 A 项目的敏感内容返回给 B 项目的人。常见做法是在 metadata 里加project_id和role字段,检索时用 filter 过滤。Chroma 支持where条件,LangChain 的 retriever 可以传search_kwargs={"filter": {"project_id": "P001"}}。这样同一套向量库服务多个项目,但每个用户只能召回自己有权查看的块。注意 filter 字段要在入库时就写好,事后补加需要重建索引。

4. 避坑与排查:审计问答系统最容易翻车的五个地方

4.1 现象:模型回答里出现文档中根本没有的条款号

原因通常是检索到的片段里没有明确条款,模型为了“显得完整”自己编了一个。审计场景对编造零容忍。解决办法是在 prompt 里明确要求“如果检索内容中没有对应条款,回答‘未找到相关依据’”,并且把 temperature 压到 0.1 以下。另外可以在生成后加一道校验,用正则检查答案里的条款号是否出现在检索片段中,不在就标记为待人工复核。

4.2 现象:同一问题两次提问答案不一致

RAG 系统如果 temperature 偏高,或者检索器每次返回的片段顺序不同,生成结果会波动。审计结论要求稳定。把 temperature 设 0 到 0.2,检索器固定k值,向量库距离度量固定为 cosine。如果还波动,检查嵌入模型是否每次加载了不同版本,或者文档入库时有没有重复块导致召回随机。

4.3 现象:表格类问题检索不到,比如“上季度管理费用前三的科目”

纯文本切块会把表格切散,向量检索对表格数字不敏感。解决办法是把表格转成 Markdown 或自然语言描述再入库,比如“科目:办公费,金额:12000,季度:Q1”。另外表格类问题更适合走结构化查询而不是向量检索,可以在系统里加一个路由,识别到“金额”“排名”“合计”这类词时转去查数据库或 Excel,而不是硬走 RAG。

4.4 现象:中文审计术语被嵌入模型当成无关词

有些嵌入模型训练语料以通用中文为主,“递延收益”“以前年度损益调整”这类术语的向量表示不够区分。换用在大规模中文语料上训练的模型,或者在入库前给术语加同义词扩展,比如把“递延收益”和“未确认融资收益”建立关联。也可以在检索后加一层关键词匹配兜底,向量召回和关键词召回各取前几,合并去重后再送给模型。

4.5 现象:文档更新后旧答案还在

向量库持久化后,文档更新了但旧块没删,检索时新旧内容同时召回,模型可能引用已废止的条款。解决办法是入库时用文档 ID 做去重,更新时先按source删除旧块再插入新块。Chroma 支持delete(where={"source": "xxx"}),在更新流程里先删后插。如果文档量大,建议维护一个入库版本表,记录每份文档的哈希和入库时间,避免重复嵌入浪费算力。

5. 进阶技巧:用重排序和答案校验把准确率再抬一档

检索返回的 top-k 片段里,真正相关的可能只有一两个,但顺序不一定对。加一个重排序模型(rerank)对召回片段重新打分,能把最相关的推到前面,生成时模型看到的依据更干净。常见做法是先用向量检索召回 20 个块,再用交叉编码器重排取前 4 个送给生成模型。交叉编码器比向量相似度慢,但只对 20 个块打分,延迟可以接受。实测在审计条款查询上,加 rerank 后答案引用正确条款的比例明显提升,尤其是问题里包含多个条件时。

# rerank_pipeline.py # 向量召回 20 个块,重排序取前 4 个,再生成答案 from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-large") def retrieve_and_rerank(query, vectordb, top_k=4, recall_k=20): # 先向量召回较多候选 candidates = vectordb.similarity_search(query, k=recall_k) # 交叉编码器对每个候选打分 pairs = [[query, doc.page_content] for doc in candidates] scores = reranker.predict(pairs) # 按分数排序取前 top_k ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in ranked[:top_k]]

recall_k=20是召回数量,太小重排序没得选,太大延迟上升。top_k=4是最终送给生成模型的数量,和之前检索器保持一致。重排序模型选中文效果好的,不要用英文模型硬排中文。注意重排序模型显存占用比嵌入模型大,如果部署在 CPU 上,20 个块的打分延迟可能到几百毫秒,需要根据实际硬件调整recall_k。

另一个技巧是答案校验。生成完答案后,用另一个轻量模型或规则检查答案里的每个关键结论是否能在检索片段中找到对应句子。找不到的结论标红提示“此结论未在依据中找到直接支持”。这一步在审计场景价值很高,因为审计人员最终要签字,任何没有依据的结论都是风险。我一般会把校验结果和答案一起展示,让人工决定是否采纳。

最后说一个我踩过的坑:早期为了追求召回率把chunk_size设得很小,结果一个完整条款被切成三段,检索时只召回中间一段,模型看到半句话就生成,答案缺前提。后来把chunk_size调到 600 并保留条款 metadata,召回率没降多少,但答案完整性好了很多。审计问答系统里,依据完整比召回数量重要,宁可少召回几个块,也不要让模型看到残缺的条款。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询