ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于大语言模型的智能审计问答系统:源码解析与部署实战

基于大语言模型的智能审计问答系统:源码解析与部署实战 简介基于大语言模型的智能审计问答系统源码适用于毕业设计、期末大作业或课程设计场景使用者可快速搭建一套完整可演示的审计问答平台。项目围绕智能审计业务设计问答逻辑代码包含清晰注释新手亦能读懂下载后简单配置即可运行前后端结构与数据文件一应俱全。资源包共34个文件、约16.77MB核心为4个Python源文件配以5个网页页面及样式脚本另含多张界面截图、图标和说明文档便于对照界面与文档理解实现细节。经过严格调试且导师认可的高分项目功能完善、界面美观管理便捷已有684人学习浏览。整体既能支撑毕设答辩与期末展示也可作为智能审计与大语言模型结合应用开发的完整参考具有较高的实际应用价值。1. 基于大语言模型的智能审计问答系统从关键词搜不到到底稿随口问做审计底稿复核的人大概都有过这种体验拿着一份三百页的应收账款底稿想查“周转率为什么下滑”用编辑器全文搜索关键数字搜出来一堆散落的凭证号就是找不到一句“坏账准备计提比例上升”这种结论性表述。这套基于大语言模型的智能审计问答系统 Python 源码解决的就是这个问题——切分、向量化、语义检索、大模型生成一条链路全串起来让审计人员直接用自然语言问底稿“今年坏账计提政策有没有变化”系统把相关段落召回后生成带来源的回答。对正在找毕业设计、期末大作业题目的学生来说它是少见的“既有完整 Python 源码又有文档说明”的项目对内审团队来说它也是一个能快速验证的本地问答原型不用把底稿上传到任何第三方服务。2. 源码结构拆解问答调用链路与三个核心模块拿到这份源码我建议不要先打开 PDF 文档从头读而是先顺着调用链把代码跑通一遍。大多数这类 Python 项目的组织方式都差不多后端起一个 HTTP 服务暴露一个/chat接口前端或者命令行把问题传进来服务内部走一遍“检索 生成”的流程最后返回回答。2.1 先看入口HTTP 接口怎么把问题接进系统一般工程里会有一个main.py或者app.py用 Flask 或 FastAPI 起服务。FastAPI 更常见一些因为自带接口文档毕业设计答辩时演示起来也方便。核心代码大致长这样from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): query: str # 用户提问如“应收账款周转率下滑的原因” top_k: int 5 # 召回相关片段数量默认取 5 段 temperature: float 0.2 # 生成参数值越低回答越稳定 app.post(/chat) async def chat(req: ChatRequest): # 简化示意真实项目中这里会先检索向量库再调大模型 return {answer: 这是回答, sources: [底稿第 12 页]}这个接口是整个系统的门面。ChatRequest里面的三个字段基本就是你要调的全部旋钮top_k控制召回上下文多少temperature控制回答风格。审计问答这种场景temperature一般不要超过 0.3不然模型容易自由发挥。从入口往后看一次请求的完整走向是HTTP 接收问题 → 向量库按相似度召回相关段落 → 把段落拼接成提示词 → 调用大模型接口 → 返回回答。按这条链路去读源码思路会清晰很多。2.2 六个职责模块从文档加载到回答生成把整份源码按职责划分通常能看到这样几个固定模块模块职责常见实现文档加载读取 PDF、Word、Excel 等底稿文件pymupdf、python-docx、pandas文本切分把长文档拆成固定长度片段带重叠自写分段函数或langchain的切分器向量化把文本片段转成向量用于语义检索sentence-transformers加载 embedding 模型向量检索计算问题与片段的相似度召回最相关段落faiss或chromadb的相似度搜索问答生成拼接上下文与问题调用大模型返回回答openai库或兼容 HTTP 调用接口层对外提供 HTTP 服务和流式输出FastAPIsse-starlette文档说明里一般会配一张调用链图代码本身的模块划分也和这张图对应。调试的时候最容易出问题的不是生成环节而是加载和切分环节——底稿里表格多、页眉页脚杂切分如果没处理好后面检索召回的就是一堆垃圾片段。2.3 配置项在哪改模型地址、向量库路径、切分参数不要到处找参数字面量统一看config.py或.env文件。典型的配置长这样# 大模型服务地址 LLM_BASE_URL http://localhost:8000/v1 LLM_API_KEY EMPTY # 本地模型服务一般不需要密钥 LLM_MODEL qwen2.5-7b-instruct # 模型名需与服务端保持一致 # Embedding 模型 EMBED_MODEL BAAI/bge-large-zh-v1.5 EMBED_DIM 1024 # 向量维度建索引时要用 # 文本切分 CHUNK_SIZE 500 # 每个片段最大字符数 CHUNK_OVERLAP 80 # 相邻片段重叠长度 # 检索参数 TOP_K 5 # 默认召回片段数 SCORE_THRESHOLD 0.35 # 相似度低于此值不返回 # 向量库路径 VECTOR_DB_PATH ./data/vector_storeLLM_BASE_URL是整份配置里最关键的一项。如果你用 Ollama 启动本地模型地址通常是http://localhost:11434/v1如果你用 vLLM 部署则是http://localhost:8000/v1。这个地址写错服务能启动但一问就报错。“EMPTY”这个值也是本地部署大语言模型场景下的惯例因为本地服务基本不校验密钥。SCORE_THRESHOLD是个容易被忽略的守卫参数低于阈值的片段宁可不要也不要硬塞给大模型。3. 检索链路与向量召回回答质量的第一道关口很多人以为问答系统的效果取决于大模型选得好不好实际调试下来会发现检索链路才是决定回答质量的第一道关口。底稿里的内容召不回来模型再强也只能胡编。3.1 切分参数怎么调审计底稿的句子密度决定 chunk 大小审计底稿和普通文章不一样它经常是“结论 数据表 凭证索引”混排。如果切得太碎一个完整结论被拦腰截断如果切得太大一段里混进多个无关主题向量化之后语义被稀释。我的经验值是CHUNK_SIZE取 500 字符左右、CHUNK_OVERLAP取 80 字符这套参数对中文底稿比较稳。切分代码逻辑如下def split_text(text: str, chunk_size: int 500, overlap: int 80) - list[str]: 按字符数切分文本相邻片段保留重叠部分避免切断完整语义。 chunks [] start 0 text_len len(text) while start text_len: end start chunk_size chunks.append(text[start:end]) if end text_len: break start end - overlap # 回退 overlap 个字符保证边界信息不丢失 return chunks注意最后三行每次切完不是从end直接继续而是回退overlap个字符再切下一段。这样做的目的是防止“应收账款余额下降了 20%”这类短语刚好处在片段边界上被切断。如果你发现召回结果里经常出现半句话优先调大overlap而不是调大chunk_size。3.2 Embedding 选型中文审计文本优先考虑 bge 系列向量化模型的选择直接决定检索效果。早期许多项目默认用text2vec或m3e但中文审计文本里表格、专有名词多这两个模型的泛化能力有点吃力。我一般推荐bge-large-zh-v1.5它对中文长文本的支持比较成熟而且在语义相似度任务上的表现相对稳定。加载方式极简from sentence_transformers import SentenceTransformer # 首次运行会自动下载模型到本地缓存之后离线可用 model SentenceTransformer(BAAI/bge-large-zh-v1.5) def embed_texts(texts: list[str]) - list[list[float]]: 批量把文本片段转成向量。 embeddings model.encode( texts, normalize_embeddingsTrue, # 归一化后直接用内积算相似度 batch_size16 ) return embeddings.tolist()这里有个选型细节bge系列在计算相似度时建议在检索式不是生成式场景下给问题加一个短指令比如把问题前缀拼成“为这个句子生成表示以用于检索相关文章”。不加前缀也能用但加上之后召回率会有一点提升。normalize_embeddingsTrue让向量变成单位向量后面用内积计算相似度时结果范围更规整阈值也好定。3.3 召回质量怎么判断用相似度分数做可视化检查排查检索问题的时候别凭感觉猜直接把召回结果和相似度分数打出来看。用一个几十行的调试脚本所有问题一目了然import json from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) # 假设这是从底稿切出来的片段列表 chunks [ 本年度公司调整信用政策坏账准备计提比例由 3% 升至 5%。, 应收账款余额较上年同期下降 12%主要由于客户回款周期缩短。, 存货周转率保持稳定未发生重大跌价迹象。 ] query 应收账款周转率下滑的原因 q_vec model.encode([query], normalize_embeddingsTrue) c_vecs model.encode(chunks, normalize_embeddingsTrue) scores (c_vecs q_vec.T).flatten() # 逐片段与问题算内积 ranked sorted(zip(scores, idxs), reverseTrue) for score, idx in ranked: print(f{score:.4f} - {chunks[idx]})这段代码把每个片段与问题的相似度分数按从高到低排出来。仔细看分数分布能判断两件事一是切分到底有没有把关键信息切成碎片二是这个 embedding 模型对当前底稿的适配程度。如果所有分数普遍低于 0.3说明文本风格和模型不适配该换模型而不是调参数。4. 问答生成与提示词工程让大模型不说车轱辘话检索做得再好提示词写得稀烂大模型照样给你输出一堆正确的废话。审计问答对答案有个硬要求能引用底稿原文。这一章讲的提示词模板和流式输出是所有演示环节里最出效果的部分。4.1 提示词模板把审计助理身份和回答约束写进去一个合格的审计问答提示词至少要包含三层信息角色身份、资料来源、回答约束。底层逻辑是让模型知道“你是审计助理不是通用聊天机器人回答必须基于给定资料”。我常用的模板是PROMPT_TEMPLATE 你是经验丰富的审计助理请基于以下底稿资料回答问题。 资料 {context} 问题{query} 回答要求 1. 只能依据资料中的信息回答资料没有的内容请明确说“底稿中未找到相关表述” 2. 涉及具体数字时引用资料中的原文数字 3. 回答控制在 300 字以内分条列出结论。 def build_prompt(query: str, context: str) - str: 把检索到的片段拼接成上下文套进模板生成最终提示词。 return PROMPT_TEMPLATE.format(queryquery, contextcontext)注意模板里的三个回答要求每一条都是在堵大模型的常见毛病。第一条堵“编造”第二条堵“数字张冠李戴”第三条堵“长篇大论”。context字段拼接检索到的片段时建议在每个片段前加一个序号和来源标记比如[片段 1 | 底稿 P12]这样模型回答时更容易引用到具体位置。4.2 流式输出把回答逐字推送到审计工作台问答系统如果等大模型生成完一整段才返回体验非常糟糕——大模型生成几百个字可能要十几秒界面一直转圈。实际工程里普遍用流式输出边生成边推送效果看起来像打字机。FastAPI 里用 SSE 实现很直接from fastapi.responses import StreamingResponse import json def stream_answer(query: str, context: str): 模拟流式输出真实项目中这里是调用大模型的 stream 接口。 def generate(): prompt build_prompt(query, context) # 以 OpenAI 兼容接口为例streamTrue 开启流式 stream client.chat.completions.create( modelLLM_MODEL, messages[{role: user, content: prompt}], streamTrue ) for chunk in stream: delta chunk.choices[0].delta.content if delta: yield fdata: {json.dumps({delta: delta}, ensure_asciiFalse)}\n\n return StreamingResponse(generate(), media_typetext/event-stream)前端拿到这个 SSE 流之后按data:前缀解析每收到一段就追加显示。这样做的另一个好处是如果某次回答开头就明显不对劲可以直接让前端中断请求省得等完整输出完才发现跑偏。4.3 本地部署大语言模型还是在线 API先想清楚审计数据去哪这是整个项目里最需要提前做决策的问题直接决定部署方式和代码里调用谁。我按实际使用场景做过一次对比维度本地部署大语言模型在线 API 调用数据安全底稿不出内网适合敏感数据数据发送到第三方服务有泄露风险部署成本需要 GPU 服务器至少 8GB 显存无需 GPU按调用量付费效果天花板受限于硬件一般用 7B14B 模型可用更大参数模型效果更稳延迟取决于显卡3090 上 7B 模型约每秒 20 字网络延迟加大模型排队波动大适合场景毕设演示、内审原型、离线环境快速验证效果、非敏感数据对这个项目来说如果你手头没有显卡最省事的路径是先用在线 API 把整条链路调通确认检索和提示词没毛病再换成本地部署。反正代码里只改LLM_BASE_URL和LLM_MODEL两个配置项逻辑一行不用动。审计数据敏感度比较高实际做项目时我几乎不会考虑把底稿全文传到云端本地部署是底线方案。5. 避坑与常见问题部署这套系统最常踩的五个坑这套系统的坑基本集中在部署初期。整理五条我自己调试时踩过、也被问过很多次的问题每条按现象到原因再到解决办法说清楚。5.1 现象启动时报端口被占用启动main.py时直接报OSError: [Errno 98] Address already in use第一次遇到会以为代码写错了。原因很简单上一次调试时服务进程没退出还占着 8000 端口。解决办法有两种# Linux / macOS 下找到占用进程并结束 lsof -i :8000 kill -9 PID # Windows 下用这个命令 netstat -ano | findstr :8000 taskkill /PID PID /F把端口杀掉之后重新启动即可。如果系统里没有lsof也可以用ss -tulnp | grep 8000查。这个坑本身不大但每次演示前不检查容易在关键时刻翻车。5.2 现象答非所问检索召回完全跑偏问“应收账款周转率下滑的原因”回答里全是存货跌价的内容。原因是召回环节出了问题大概率是 embedding 模型加载失败后用了随机初始化向量或者CHUNK_SIZE切得过大导致片段语义混杂。解决方法是先跑一遍 3.3 的相似度调试脚本看召回片段和分数是否合理再检查config.py里的EMBED_MODEL是否确实加载成功。模型加载失败时sentence-transformers一般会抛异常但如果代码里做了异常吞掉后续检索就会静默返回垃圾结果。查这个问题时最忌讳跳过检索直接看最终回答始终要先确认召回环节的输出。5.3 现象答案里出现了底稿上不存在的数字这是大模型幻觉的典型表现。底稿明明没写坏账率模型却回答“坏账准备计提比例上升至 5%”。究其原因一是提示词没加约束二是temperature设太高三是召回上下文不够导致模型自己补全。解决方法是三层并治把temperature降到 0.2 以下在提示词里明确要求“资料没有的内容请说未找到”把TOP_K从 5 提到 8给模型更多可引用的真实素材。如果你想做得更稳一点可以让系统把召回的片段原文拼在回答后面审计人员一眼就能核对。5.4 现象PDF 加载出来全是方框乱码加载扫描版底稿时经常出现这个问题——文本提取出来不是中文而是一堆类似乱码的方框字符。原因是这些 PDF 本质是图片没有文本层常规pymupdf文本提取拿不到内容。解决办法是先把页面转成图片再做 OCR 识别。常见做法是用rapidocr_onnxruntime或 PaddleOCR识别后再交给切分模块。我的经验是扫描版底稿的 OCR 识别结果里经常夹杂识别错误最好在切分前做一轮简单清洗把全角空格和常见错字替换掉否则这些噪声会直接进入向量库。5.5 现象没 GPU推理慢到没法用本地部署大语言模型时没有显卡加载 7B 模型生成一个小句可能要几十秒演示效果极差。解决思路有两个一是换小模型走 CPU 推理比如qwen2.5-1.5b-instruct量化版生成速度会明显提升代价是回答质量略降二是把大模型服务放在有 GPU 的服务器上本机只跑检索和前端这样展示效果和运行速度都能兼顾。毕设答辩场景下我倾向于后者——借一台 3090 级别的服务器部署qwen2.5-7b-instruct本机连它的地址就行。6. 进阶用法用批量脚本压测问答效果把检索质量量化单条问答调试通过只算完成了一半想安心交差得用批量脚本把效果量化出来。我习惯针对底稿预置二十个左右的高频审计问题比如“存货跌价准备计提是否充分”“固定资产折旧政策有无变更”然后循环调用接口把回答全部落盘人工抽查一遍覆盖率。6.1 批量问答脚本把测试题一口气跑完import requests import json questions [ 应收账款周转率下滑的原因是什么, 坏账准备计提比例本年度有无调整, 存货跌价准备计提是否充分, 固定资产折旧政策有无变更 ] results [] for q in questions: resp requests.post( http://localhost:8000/chat, json{query: q, top_k: 5, temperature: 0.2}, timeout120 ) data resp.json() results.append({question: q, answer: data[answer]}) print(fQ: {q}\nA: {data[answer][:150]}\n{- * 40}) with open(qa_results.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)这个脚本的价值在于把主观感受变成可以对比的记录。超时时间设 120 秒避免某个问题卡死拖垮整个压测。落盘的qa_results.json在答辩时可以展示成一张问答记录表比现场随机提问有说服力得多。6.2 把答案带上下文写进审计底稿备注批量压测之后你会发现有些问题回答得不够细这时候要回到检索链路去看召回片段是否命中了真正的结论段落。高阶一点的做法是让接口在返回答案的同时把召回的原始片段一并返回审计人员可以直接对照来源判断回答的可信度。在那之后我每次接到新的问答场景都会先跑一遍批量脚本看回答覆盖率再回来调切分参数和提示词而不是盲目换更大的模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表