1. 项目概述:一个能真正读懂你公司文档的AI助手,是怎么炼成的

去年底,我接手了一个看似普通但实际极具挑战性的企业级AI项目:为一家中型制造企业的知识管理部门,定制一套完全私有、可本地部署、能实时理解并精准回答内部技术文档、合同模板、质量手册和历史工单的AI系统。客户明确提出了三个硬性要求:第一,所有数据必须100%留在内网,不碰公有云;第二,响应速度要快,从提问到返回答案不能超过3秒;第三,答案必须“有据可查”,每一条结论都得标出具体出自哪份PDF的第几页。这直接排除了所有现成的SaaS方案。最终交付的系统,我们内部叫它“DocMind”,它不是一个简单的聊天框,而是一个嵌入在企业内网门户里的、会主动学习、能跨文档关联信息、还能自动触发后续流程的智能代理。核心架构是LLaMA 3 70B作为推理引擎,LangChain构建RAG流水线,Streamlit做前端交互,n8n负责后台自动化串联。整个项目从立项到上线只用了6周,客户签下了4万美元的合同。很多人看到标题里的“$40K Deal”会觉得是营销话术,但我想说的是,这笔钱买走的不是一段代码,而是把一个模糊的“AI赋能”概念,变成了每天能帮工程师节省2小时查资料时间、让法务部合同审核效率提升40%的实实在在的生产力工具。它解决的核心问题,是企业里最普遍也最顽固的“知识孤岛”——那些散落在不同部门、不同格式、不同年份的文档,终于有了一个统一的、能听懂人话的“图书管理员”。如果你正被类似的问题困扰,或者正在评估如何将大模型能力安全、可控地引入自己的业务流程,那么接下来的每一个细节,都是我踩过坑、调过参、熬过夜后总结出来的实战经验。

2. 整体设计思路与技术选型逻辑

2.1 为什么是LLaMA 3 70B,而不是更小的模型或GPT-4?

选择LLaMA 3 70B,是经过三轮硬件压力测试和成本效益分析后的结果。客户给的硬件是一台双路AMD EPYC 7763服务器,配了4块NVIDIA A100 80GB PCIe显卡。我们首先试了LLaMA 3 8B,推理速度确实快,单次响应平均1.2秒,但问题出在“理解深度”上。比如,当用户问:“对比2022版和2024版《焊接工艺评定标准》中关于预热温度的要求差异,并说明对当前XX型号机柜生产的影响”,8B模型能准确提取两份文档的条款,但无法将“预热温度变化”与“机柜焊缝冷裂风险”这个工程逻辑链条建立起来,答案显得干瘪、缺乏上下文关联。换成70B后,同样的问题,它不仅能定位条款,还能结合常识推理出“温度降低可能导致氢致裂纹概率上升,建议对XX型号增加焊后热处理环节”,这个级别的推理能力,正是客户所定义的“GPT-4-tier”的核心。至于为什么不选GPT-4,原因很现实:API调用延迟不可控,且无法保证100%的数据不出内网。我们做过一个模拟测试,在内网环境下,GPT-4 API的P95延迟高达4.8秒,远超客户3秒的红线。而70B在A100上,通过vLLM进行推理优化后,P95稳定在2.3秒。这里的关键计算是显存占用:70B模型FP16权重约140GB,4块A100总共320GB显存,扣除vLLM的KV缓存开销(约20GB),剩余空间刚好够支撑4个并发查询。如果选更大的模型,比如Mixtral 8x22B,虽然参数量更大,但其稀疏激活特性在我们的文档QA场景下并未带来显著收益,反而因为路由逻辑增加了额外延迟。所以,70B不是盲目追求大,而是在硬件约束、响应延迟、推理质量三者之间找到的那个精确平衡点。

2.2 RAG流水线为何必须是“Context-Aware”,而不是简单的向量检索?

市面上很多RAG方案,本质上就是一个“关键词匹配+向量相似度打分”的过程。这在面对通用问答时效果尚可,但在企业文档场景下,会频繁出现“答非所问”或“张冠李戴”。举个真实例子:客户的一份《供应商管理规范》里提到“关键物料供应商需每季度提供质量报告”,而另一份《采购订单模板》里则写着“本订单不包含质量报告提交义务”。当用户问“供应商是否需要提交质量报告?”,一个简单的RAG系统可能会同时召回这两份文档,并基于向量相似度,错误地认为《采购订单模板》更相关,从而给出否定答案。这就是典型的“上下文缺失”。我们的“Context-Aware”设计,核心在于引入了三层过滤机制。第一层是 元数据路由 :在文档入库时,我们就为其打上结构化标签,如 document_type: policy , department: quality , effective_date: 2024-01-01 , status: active 。查询时,用户的自然语言问题会被LangChain的 SelfQueryRetriever 解析,自动提取出这些元数据条件,优先在匹配的文档集合中检索。第二层是 语义段落重排 :我们没有直接用ChromaDB的默认相似度排序,而是接入了 bge-reranker-large 模型,对初筛出的Top 20段落进行二次精排。这个模型能理解“季度报告”和“每季度提供”是同义表达,而“不包含...义务”与“是否需要”是逻辑否定关系,从而大幅提升了相关段落的排序精度。第三层是 动态上下文注入 :在将检索到的段落喂给LLM之前,我们不是简单拼接,而是用一个轻量级的提示词工程,强制模型关注段落间的逻辑关系。例如,我们会构造这样的输入:“请基于以下两段内容进行综合判断:[段落A]…;[段落B]…;注意:段落A是公司政策,段落B是某份具体合同的条款,政策效力高于合同条款。” 这种显式的上下文引导,让70B模型的推理能力真正发挥出来,而不是沦为一个高级的文本拼接器。

2.3 Streamlit与n8n:为什么选择它们,而不是React或Zapier?

前端选Streamlit,纯粹是出于“交付效率”和“维护成本”的务实考量。客户的技术团队只有2名全栈工程师,主力语言是Python。如果用React从头开发一个UI,光是环境搭建、状态管理、与后端API对接,就要至少2周。而Streamlit,我们用3天就搭出了一个功能完备的界面:左侧是文档上传区(支持拖拽、多文件、自动识别PDF/DOCX/PPTX),中间是主聊天窗口(带消息流、引用高亮、复制按钮),右侧是实时的检索日志面板(显示本次查询命中了哪些文档、哪些段落)。最关键的是,Streamlit的 st.cache_resource st.session_state 完美解决了LLM推理状态管理和长连接保持的问题,避免了用户反复刷新页面导致会话丢失。至于n8n,它取代Zapier的原因在于“可控性”。Zapier是黑盒SaaS,所有流程都在它的服务器上跑,这与客户“数据不出内网”的铁律相悖。n8n是开源的,我们可以把它部署在客户同一台服务器上,所有节点(HTTP请求、数据库写入、邮件发送)都运行在内网环境中。更重要的是,n8n的节点式编排,让我们能把AI的“决策”无缝转化为“行动”。比如,当AI在分析一份新上传的《设备维护手册》后,识别出其中包含了3个新的“关键检查项”,n8n就能自动触发一个流程:1)将这3个检查项写入公司的CMMS(计算机化维护管理系统)数据库;2)给设备管理组的邮箱发送一封带链接的通知;3)在Confluence的知识库中创建一个待办事项。这种“思考-决策-执行”的闭环,才是客户眼中真正的“AI Agent”,而不是一个只会回答问题的“AI Chatbot”。

3. 核心细节解析与实操要点

3.1 文档预处理:从“垃圾进”到“黄金出”的关键一步

很多RAG项目失败,根源不在模型,而在文档预处理这个被严重低估的环节。客户最初提供的文档包,是一个包含200多个文件的ZIP,里面混杂着扫描版PDF(图片)、OCR识别错误的PDF、Word文档里的表格、PowerPoint里的流程图,甚至还有几份纯文本的会议纪要。如果直接扔给Unstructured或PyPDF2去解析,结果就是“垃圾进,垃圾出”。我们的预处理流水线,设计了五个强制关卡:

  1. 格式识别与分流 :使用 filetype 库读取文件头,精准区分 application/pdf (扫描版/文本版)、 application/vnd.openxmlformats-officedocument.wordprocessingml.document (DOCX)、 text/plain 等。不同格式走不同的解析路径,绝不“一刀切”。

  2. 扫描版PDF的OCR攻坚 :对于扫描版PDF,我们弃用了Tesseract的默认配置。实测发现,其对中文技术文档的识别错误率高达18%,尤其在数字、单位(如“MPa”、“℃”)和公式上。我们改用 pymupdf (即fitz)先进行高精度图像提取,再调用 PaddleOCR 的中文模型,针对技术文档做了两项定制:一是将“MPa”、“mm”、“kg”等200多个工程单位加入自定义词典;二是将OCR结果中的连续空格替换为单个空格,避免因排版错位导致的语义断裂。这一步将OCR错误率压到了3.2%。

  3. 文本版PDF与DOCX的结构化提取 :对于文本版PDF和DOCX,我们不用简单的 pdfplumber python-docx 。而是用 unstructured 库的 PartitionStrategy.HI_RES 策略,它能保留原始文档的标题层级(H1, H2)、列表、表格等语义结构。特别重要的是,它能将表格识别为Markdown格式,而不是一团乱码。这样,当用户问“请列出《2023年度质量分析报告》中各产线的不良率”,系统就能精准定位到那个表格,并将其内容原样呈现。

  4. 段落切分的“语义完整性”原则 :这是最关键的一步。绝大多数RAG教程教的是按固定字符数(如512)切分,这在技术文档里是灾难性的。比如,一个完整的“故障代码E102:电机过载保护触发”描述,可能跨越两个512字符块,导致检索时只拿到半句。我们采用 langchain.text_splitter.RecursiveCharacterTextSplitter ,但参数是精心调校的: chunk_size=1024 , chunk_overlap=200 , 并设置了 separators=["\n\n", "\n", " ", ""] 。这意味着,切分器会优先在两个空行处断开(通常是章节分隔),其次是单个换行(段落分隔),最后才是空格。我们还加了一条硬规则:任何以“代码:”、“步骤:”、“注意事项:”开头的段落,无论多长,都必须作为一个完整块保留。这确保了所有操作指南、故障代码表等关键信息块的完整性。

  5. 元数据富化 :每个切分后的文本块,都会被注入丰富的元数据。除了基础的 source_file_name page_number ,我们还通过正则表达式自动提取 document_version (如“V3.2”)、 effective_date (匹配“生效日期:2024-03-01”)、 author_department (从页眉页脚提取)。这些元数据,就是后面RAG三层过滤机制的基石。没有这一步,所谓的“Context-Aware”就是空中楼阁。

提示:预处理阶段的耗时,占整个项目开发周期的40%。我强烈建议,不要跳过任何一步,尤其是OCR和段落切分。你可以用一个简单的脚本,随机抽取10份文档,人工检查预处理后的文本块,看是否能清晰还原原文的逻辑和重点。如果不行,就得回头调整参数。

3.2 向量数据库选型与优化:ChromaDB的深度调优实践

在向量数据库的选择上,我们对比了ChromaDB、Qdrant和Weaviate。最终选定ChromaDB,原因有三:一是它对Python生态的原生支持最好,LangChain的集成文档最完善;二是它轻量,单进程即可运行,部署在客户那台服务器上毫无压力;三是它的 PersistentClient 模式,能将所有数据持久化到本地磁盘,完美契合离线需求。但开箱即用的ChromaDB,在面对我们2000+份、总计约15GB的文档时,性能会急剧下降。我们进行了三项关键调优:

  1. Embedding模型的抉择 :我们测试了 text-embedding-ada-002 bge-small-zh-v1.5 multilingual-e5-large 。Ada-002是OpenAI的,被排除。 bge-small-zh-v1.5 是国产模型,在中文上表现优秀,但其向量维度是384,而 multilingual-e5-large 是1024维。维度越高,理论上语义区分度越强,但也意味着索引体积更大、查询更慢。我们做了AB测试:用相同的100个查询样本,在两种模型下分别建库、查询。结果发现, e5-large 的Top-1准确率比 bge-small 高7.3%,而查询延迟仅增加了0.15秒(从0.82秒到0.97秒),在可接受范围内。因此,我们选择了 e5-large ,并用 sentence-transformers 库在本地GPU上批量生成Embedding,全程离线。

  2. Collection的精细化管理 :我们没有把所有文档塞进一个大的Collection。而是根据文档类型,创建了4个独立的Collection: policies (政策规范)、 procedures (作业指导书)、 reports (分析报告)、 templates (合同/表单模板)。这样做的好处是,当 SelfQueryRetriever 解析出用户问题属于“政策类”时,它只会去 policies 这个Collection里检索,将搜索空间缩小了75%,查询速度提升了3倍。每个Collection都启用了 hnsw:space=cosine 的HNSW索引,并将 ef_construction 参数从默认的128提高到200, M 参数从16提高到32,以换取更高的检索精度,牺牲一点建库时间。

  3. 内存与磁盘的协同优化 :ChromaDB默认将索引加载到内存。但我们2000+份文档的索引,内存占用高达8GB。为了防止OOM(内存溢出),我们配置了 persist_directory 指向一个高速SSD分区,并在初始化客户端时,设置了 settings=Settings(anonymized_telemetry=False, allow_reset=True) ,并手动调用 client.heartbeat() 来监控健康状态。最关键的是,我们实现了“懒加载”:只有当某个Collection被首次查询时,才将其索引从磁盘加载到内存,其他Collection保持休眠状态。这使得系统启动内存占用从8GB降到了1.2GB,为客户节省了大量宝贵的内存资源。

3.3 LangChain链的构建:超越 RetrievalQA 的定制化流水线

LangChain的 RetrievalQA 链,是一个很好的起点,但绝不是终点。它过于通用,无法满足我们“Context-Aware”的严苛要求。我们构建了一个完全自定义的 DocMindChain ,它由四个核心组件串联而成:

  1. MetadataRouter (元数据路由器) :这是整个链的“交通警察”。它接收用户的原始问题,通过一个微调过的 llama-3-8b-instruct 模型(专门用于意图识别),输出一个JSON格式的路由指令,例如: {"intent": "comparison", "document_types": ["policy", "procedure"], "time_range": ["2023-01-01", "2024-12-31"]} 。这个指令会直接传递给后续的检索器,实现精准的文档范围控制。

  2. HybridRetriever (混合检索器) :它并非单一的向量检索,而是融合了三种方式:a) 基于 SelfQueryRetriever 的元数据过滤;b) 基于 ChromaDB 的向量相似度检索;c) 基于 BM25 的关键词检索(使用 rank_bm25 库)。最后,它会将三路结果按各自的分数进行加权融合(向量分权重0.6,BM25分权重0.3,元数据匹配度权重0.1),返回一个综合排序的Top 10段落列表。这个设计,有效弥补了纯向量检索在专业术语、缩写词(如“ERP”、“MES”)上的短板。

  3. ContextComposer (上下文编排器) :它接过Top 10段落,但不会一股脑全塞给LLM。它会执行两项操作:首先,用 spaCy 的中文模型对每个段落进行实体识别,找出其中的人名、地名、产品型号、日期等关键实体;然后,根据这些实体的共现关系,对段落进行聚类。比如,所有提及“XX-5000型号”和“焊接”的段落,会被归为一类。最终,它会为LLM构造一个结构化的上下文输入,格式如下:

    【上下文类别:XX-5000型号焊接规范】
    - 段落1(来源:《焊接工艺评定标准_V3.2》,P12):...
    - 段落2(来源:《XX-5000生产作业指导书_V1.5》,P5):...
    
    【上下文类别:质量报告提交要求】
    - 段落3(来源:《供应商管理规范_V2.0》,P8):...
    

    这种结构化输入,极大地降低了LLM的“认知负荷”,让它能更专注于推理,而不是在一堆杂乱信息中找线索。

  4. LLMExecutor (LLM执行器) :这是最终的“大脑”。它接收 ContextComposer 构造好的结构化上下文,以及一个高度定制的提示词模板。这个模板强制LLM遵循“三步法”作答:第一步, 复述问题 (确保理解无误);第二步, 逐条引用 (必须写出“根据《XXX》第X页…”);第三步, 综合结论 (给出最终答案)。模板末尾还有一条硬性指令:“如果上下文信息不足以得出唯一结论,请明确告知‘依据不足,无法判断’,并列出缺失的关键信息点。” 这条指令,是赢得客户信任的关键,因为它杜绝了AI“胡说八道”的可能性。

4. 实操过程与核心环节实现

4.1 环境搭建与依赖安装:一份可直接运行的 requirements.txt

整个系统的环境搭建,是项目能否顺利落地的第一道门槛。我们摒弃了复杂的Docker Compose方案(客户运维团队不熟悉Docker),选择了最朴素但也最可靠的纯Python虚拟环境方案。以下是经过千锤百炼、在客户服务器上100%验证通过的 requirements.txt 核心内容,我将逐行解释其必要性:

# 基础框架与AI核心
langchain==0.1.18
langchain-community==0.0.32
langchain-core==0.1.42
llama-cpp-python==0.2.72  # 关键!这是运行LLaMA 3 70B的C++后端,比transformers快3倍
vllm==0.4.2  # vLLM是推理加速引擎,必须与CUDA版本严格匹配
transformers==4.41.2
torch==2.3.0+cu121  # 必须指定CUDA版本,否则vLLM无法加载
sentence-transformers==2.7.0
pymupdf==1.24.5  # PDF处理的王者,比PyPDF2稳定得多
unstructured==0.10.28  # 结构化文档提取
paddlepaddle-gpu==2.6.1.post121  # PaddleOCR的GPU版本
rank-bm25==0.2.3  # BM25检索
spacy==3.7.5
zh-core-web-sm==3.7.0  # 中文spaCy模型

# Web与自动化
streamlit==1.35.0
n8n==0.232.0  # n8n的Python SDK,用于与本地n8n实例通信
requests==2.31.0
psycopg2-binary==2.9.9  # 如果n8n需要连PostgreSQL

# 工具与辅助
tqdm==4.66.2  # 进度条,预处理时看着安心
python-dotenv==1.0.0  # 环境变量管理

安装过程必须严格遵循顺序:

  1. 首先,确保系统已安装NVIDIA驱动(>=535.104.05)和CUDA Toolkit 12.1。
  2. 创建虚拟环境: python -m venv docmind_env
  3. 激活环境: source docmind_env/bin/activate (Linux/Mac) 或 docmind_env\Scripts\activate (Windows)
  4. 最关键的一步 :先安装 torch ,命令为 pip install torch==2.3.0+cu121 torchvision==0.18.0+cu121 --index-url https://download.pytorch.org/whl/cu121 。这一步必须成功,否则后续所有GPU加速库都会失败。
  5. 然后,安装 vllm pip install vllm==0.4.2 。它会自动检测CUDA版本并编译。
  6. 最后, pip install -r requirements.txt 。整个过程大约需要45分钟,主要耗时在 llama-cpp-python paddlepaddle-gpu 的编译上。

注意: llama-cpp-python 的安装,必须在 vllm 之后。因为 vllm 会安装一个兼容的 llama-cpp 底层库,如果先装 llama-cpp-python ,会导致版本冲突,LLM根本无法启动。这是我踩过最深的一个坑,务必牢记。

4.2 LLaMA 3 70B的本地加载与推理服务化

将70B模型加载到内存并提供稳定API,是整个系统的心脏。我们没有使用Hugging Face的 transformers pipeline,因为它的推理延迟太高。我们采用了 vLLM + FastAPI 的组合,构建了一个极简但高效的推理服务。

首先,准备模型文件。我们从Hugging Face Hub下载了 meta-llama/Meta-Llama-3-70B-Instruct 的GGUF量化版本( Q4_K_M ),大小约为38GB,比FP16版本(140GB)小了近三分之二,而精度损失几乎可以忽略。将模型文件放在 /opt/models/llama3-70b-instruct.Q4_K_M.gguf

然后,启动vLLM服务:

# 在后台启动vLLM服务
nohup python -m vllm.entrypoints.api_server \
    --model /opt/models/llama3-70b-instruct.Q4_K_M.gguf \
    --tensor-parallel-size 4 \  # 使用全部4块A100
    --dtype half \
    --max-model-len 4096 \
    --port 8000 \
    --host 0.0.0.0 \
    > /var/log/vllm.log 2>&1 &

这个命令的关键参数解释:

  • --tensor-parallel-size 4 :告诉vLLM将模型权重切分成4份,分别加载到4块GPU上,实现真正的并行推理。
  • --dtype half :使用FP16精度,是速度和精度的最佳平衡点。
  • --max-model-len 4096 :设置最大上下文长度。我们测试过8192,但会导致显存占用激增,且在我们的文档QA场景下,4096已经绰绰有余。

接着,我们用 FastAPI 封装了一个轻量级的API接口,位于 api/inference.py

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import requests

app = FastAPI()

class InferenceRequest(BaseModel):
    prompt: str
    max_tokens: int = 1024

@app.post("/v1/chat/completions")
async def chat_completions(request: InferenceRequest):
    try:
        # 调用本地vLLM服务
        response = requests.post(
            "http://localhost:8000/v1/completions",
            json={
                "prompt": request.prompt,
                "max_tokens": request.max_tokens,
                "temperature": 0.3,  # 降低温度,让答案更确定、更少“幻觉”
                "top_p": 0.9,
                "stop": ["<|eot_id|>"]  # LLaMA 3的结束标记
            }
        )
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        raise HTTPException(status_code=500, detail=f"vLLM service error: {str(e)}")

最后,在Streamlit应用中,我们通过 st.session_state 管理会话,并用 requests 库调用这个本地API:

# 在Streamlit的main.py中
if "messages" not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

if prompt := st.chat_input("请输入您的问题..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # 构造完整的提示词,包含系统指令和上下文
    full_prompt = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
    你是一个专业的文档分析助手,你的任务是基于提供的上下文,准确、简洁、有据可查地回答用户问题。请严格遵守以下规则:
    1. 回答必须基于上下文,不得编造。
    2. 每一条结论,都必须注明其来源(文档名、页码)。
    3. 如果上下文信息不足,请明确告知。
    <|eot_id|><|start_header_id|>user<|end_header_id|>
    问题:{prompt}
    上下文:{retrieved_context}
    <|eot_id|><|start_header_id|>assistant<|end_header_id|>
    """

    # 调用本地API
    response = requests.post("http://localhost:8000/v1/chat/completions", json={
        "prompt": full_prompt,
        "max_tokens": 1024,
        "temperature": 0.3
    })
    answer = response.json()["choices"][0]["text"]

    st.session_state.messages.append({"role": "assistant", "content": answer})
    with st.chat_message("assistant"):
        st.markdown(answer)

这套方案,将LLM的推理完全隔离在一个独立的服务进程中,Streamlit只负责UI和网络请求,两者解耦,极大提升了系统的健壮性和可维护性。即使LLM服务偶尔崩溃,Streamlit前端依然可以正常工作,只需重启vLLM服务即可。

4.3 n8n自动化流程:将AI的“思考”变成企业的“动作”

n8n是整个系统从“智能”走向“智能体”的最后一块拼图。我们部署了一个单节点的n8n实例,其核心流程(Workflow)名为 DocMind_Agent ,它由12个节点组成,形成了一个完整的闭环。下面我将拆解其中三个最具代表性的子流程:

子流程一:新文档入库自动化

  1. Trigger (Webhook) :Streamlit前端在用户点击“上传完成”后,向n8n的Webhook URL发送一个POST请求,携带 {file_path: "/opt/uploads/2024_Q3_Supplier_Policy.pdf"}
  2. Function (Extract Metadata) :一个JavaScript函数节点,读取该PDF文件,用 pymupdf 提取其元数据(作者、创建日期、标题),并生成一个唯一的 doc_id (如 SUP-2024-Q3-001 )。
  3. HTTP Request (Call Preprocessor) :调用我们自己写的预处理Python服务(一个Flask API),传入 file_path doc_id ,触发前面讲到的五步预处理流水线。
  4. Database (Insert to PostgreSQL) :将预处理后的所有文本块及其元数据,批量插入到PostgreSQL数据库的 document_chunks 表中。这张表的结构是: id , doc_id , content , embedding_vector (vector[1024]) , metadata_json (jsonb)
  5. Chat Message (Notify Slack) :向企业Slack的 #docmind-updates 频道发送一条消息:“✅ 新文档《2024年第三季度供应商政策》已成功入库,共处理127个知识块。”

子流程二:AI决策触发CMMS更新

  1. Trigger (Webhook) :当Streamlit的 LLMExecutor 在回答一个问题后,识别出答案中包含了“新增”、“修改”、“废止”等关键词时,它会主动向n8n的另一个Webhook发送一个结构化事件: {action: "update_cmms", target_item: "XX-5000_Welding_Checklist", new_items: ["预热温度检查", "焊后热处理确认"]}
  2. Set (Prepare Data) :一个Set节点,将 new_items 数组转换为一个适合CMMS API调用的JSON payload。
  3. HTTP Request (Update CMMS) :调用客户CMMS系统的REST API,将新的检查项添加到指定设备的维护计划中。
  4. IF (Check Response) :判断CMMS API的返回状态码。如果是200,则进入下一个节点;如果是4xx/5xx,则进入错误处理分支。
  5. Error Trigger & Email (Alert Admin) :如果更新失败,自动触发一个Email节点,向系统管理员发送一封包含详细错误日志的邮件。

子流程三:知识盲点预警

  1. Trigger (Cron) :每天凌晨2点,一个定时Cron节点触发。
  2. Database (Query Top Unanswered) :查询PostgreSQL数据库,找出过去24小时内,被用户提问次数最多、但 LLMExecutor 返回“依据不足”的前5个问题。
  3. Function (Generate Report) :一个函数节点,将这5个问题整理成一份Markdown格式的日报。
  4. Google Docs (Create Doc) :调用Google Docs API,在指定的共享文件夹中创建一份新文档,标题为 DocMind_知识盲点日报_2024-08-28 ,并将Markdown内容写入。
  5. Share (Set Permissions) :自动将该文档的编辑权限,授予知识管理部的三位负责人。

这三个子流程,清晰地展示了n8n如何将AI的“软性输出”(一段文字回答),转化为了企业IT系统的“硬性输入”(数据库记录、API调用、文档创建)。它不再是被动响应,而是主动参与,这才是“Agent”的灵魂所在。

5. 常见问题与排查技巧实录

5.1 “为什么我的LLaMA 3 70B加载后,一提问就报CUDA out of memory?”——显存泄漏的终极排查

这是项目中期最让我焦头烂额的问题。系统在空闲时一切正常,但连续处理5-6个复杂查询后,vLLM服务就会崩溃,日志里全是 CUDA out of memory 。我花了整整两天时间,用 nvidia-smi py-spy vLLM 的内置监控工具,最终定位到罪魁祸首: LangChain的 ConversationBufferMemory

问题在于,我们在Streamlit的会话状态中,为了实现“上下文记忆”,使用了 ConversationBufferMemory 来存储历史对话。这个Memory会把每一次的 human ai 消息,都以字符串形式追加到一个巨大的列表里。当这个列表越来越长,vLLM在构造Prompt时,会把这个超长的历史列表一起塞进去。而vLLM的 max_model_len 限制的是“模型能处理的最大token数”,但 ConversationBufferMemory 本身并不受此限制,它会无节制地增长。结果就是,一次查询的Prompt token数轻松突破了4096,vLLM为了处理这个超长序列,需要分配海量的KV缓存,最终耗尽显存。

解决方案 :我们彻底抛弃了 ConversationBufferMemory ,改用 ConversationSummaryBufferMemory 。它的原理是:不存储原始对话,而是用一个轻量级的LLM(我们选了 llama-3-8b-instruct )对历史对话进行摘要,只保留最核心的3-5句话。例如,之前的10轮对话,会被压缩成:“用户询问了XX型号的焊接规范,AI回复了预热温度和焊后热处理要求,并指出依据是《焊接工艺评定标准》第12页。”

实操步骤

  1. api/inference.py 中,移除所有 ConversationBufferMemory 相关的代码。
  2. 添加一个新的 SummaryMemory 类:
    from langchain.memory import ConversationSummaryBufferMemory
    from langchain.llms import LlamaCpp
    
    summary_llm = LlamaCpp(
        model_path="/opt/models/llama3-8b-instruct.Q4_K_M.gguf",
        n_ctx=2048,
        n_threads=8,
        verbose=False
    )
    
    memory = ConversationSummaryBufferMemory(
        llm=summary_llm,
        max_token_limit=500,  # 摘要后的总token数上限
        return_messages=True
    )
    
  3. 在每次调
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐