先抛出今天这篇文章的核心观点:AI 之所以“不瞎编了”,不是因为模型突然变聪明了,而是因为工程上给它加了一圈“必须查资料、必须走流程、不允许自由发挥”的护栏。
这圈护栏并不是某一个框架能独立完成的。它在真实落地中往往由多层开源基建组成:有存放知识的统一工作区,有在端侧快速做判断的小模型,有本地部署的大模型运行时,有负责召回证据的检索引擎,还有把复杂业务关系拆成路径的决策图谱。
下面我会把这套“防幻觉基建”拆开来讲,每个环节都会给出开源代表项目、适合场景、关键配置和可以直接参考的示例。文章涉及 Docker、Python 命令较多,适合正在做 RAG、Agent 或企业知识库的开发者阅读。
1. 先搞清楚:大模型为什么会一本正经地胡说八道
“AI 瞎编”在技术圈有个专门的词叫幻觉(Hallucination),意思是模型生成了流畅、自信,但事实上不成立的内容。解决幻觉之前,必须先理解它的来源。
其实原因并没有多玄。大模型本质上是一个基于概率的文本生成系统,它每生成一个字,都是在计算“在已知上文和训练参数的情况下,下一个字最像什么”。它并没有一个像数据库那样的存储引擎,也不具备“去某个系统里查一下订单状态”的原生能力。当用户问的内容在模型记忆里根本不存在,或只是部分存在,模型就会用语言表达习惯“补”出一个答案。
这就解释了为什么会出现两类非常典型的幻觉:
第一类是“编政策”。比如公司内部的退换货规则只有 200 个字,散落在某个 Word 文档里。模型不在文档上运行,它不知道具体条款,但只要问的人足够多,它就会按自己对“一般退换货政策”的理解生成一段看似合理的答案。
第二类是“编引用”。模型在回答中列出了看起来非常正式的功能模块、供应商名称甚至金额,但实际上这些字段并不存在于你的业务系统中。这是最危险的一类幻觉,因为它会让读者误以为模型做过系统查询。
这里也要说清楚一个误区:并不是把所有资料喂给模型做一次微调,幻觉就消失了。微调适合改变模型的语言风格、输出格式、角色设定,但很难解决“企业知识持续更新”的问题。今天新增一份制度文档,明天调整一项计价规则,不可能每次都重新训练。
所以,业界的做法不是强迫模型记住更多,而是把“回答”拆成“让模型去查、去选、去组织”。模型负责最后一步表达,前面的知识获取、证据筛选、路径校验交给外部基建完成。
2. 防幻觉的五层架构:先看整体再逐个拆解
要构建一个基本不会“瞎编”的问答系统,常见的方式是把能力拆成五层。每一层解决一类问题:
| 层级 | 解决什么问题 | 典型开源代表 | 防幻觉原理 |
|---|---|---|---|
| 统一工作区 | 知识散落在多个系统,模型没有统一入口 | AnythingLLM | 把上传文档切块、建索引并隔离到不同 Workspace |
| 端侧小模型 | 每个问题都直接调用大模型,成本高且不可控 | ONNX Runtime + TinyBERT/ALBERT 类模型 | 先做分类、抽取、拦截,减少大模型自由发挥 |
| 本地大模型运行时 | 数据需要私有化,API 调用不方便 | Ollama | 让模型运行在企业环境内,便于配合固定系统提示词 |
| 检索引擎 | 模型无法主动定位资料 | Meilisearch / Elasticsearch / Milvus | 先召回候选段落,再交给模型总结 |
| 决策图谱 | 多跳逻辑问题无法靠相似度检索解决 | GraphRAG / Neo4j / NetworkX | 把业务关系建模为图路径,让模型按照路径推导 |
当用户发起一个问题时,比较理想的流程是这样的:
- 请求先进入一个端侧小模型,判断这是知识类问题、闲聊类问题还是不适合由大模型回答的问题。
- 知识类问题进入检索层,在统一工作区或企业文档中找到最相关的 3 到 5 个段落。
- 如果问题存在明确因果关系,比如“取消订单后几天能到账”,还需要从决策图谱中取出关系路径。
- 最终,页面把这些证据拼接成一个完整上下文,传给本地大模型。
- 大模型只允许依据上下文回答。如果上下文里没有答案,必须输出“资料中没有找到”。
下面逐个介绍每一层的侧重点和落地方法。
3. 统一工作区:AnythingLLM 让知识不再流浪
“模型回答不到点子上”最常见的原因,其实是资料在模型面前根本没有一个统一入口。很多团队的知识分散在网页、PDF、Excel、Notion 和聊天记录里,最后开发人员手动复制一段塞进 Prompt。这种方式既做不到系统化更新,也没有隔离能力。
AnythingLLM 是一个开源的全栈 AI 工作区,它把“上传文档、向量化、切片、建立索引、对话”集中到了同一套界面中。它最核心的概念是 Workspace。每个 Workspace 相当于一个独立的知识空间,你可以把某个产品线的文档放进一个 Workspace,把另一个业务线的文档放进另一个 Workspace。对话时只让模型读取当前 Workspace,避免不同业务上下文互相污染。
这也直接降低了幻觉概率:当模型面对的内容边界是清晰的,它就不容易把 A 业务的规则套到 B 业务上。
先来看一个最基本的部署方式。使用 Docker Compose 前,先在服务器上创建一个目录:
mkdir -p ~/anythingllm && cd ~/anythingllm然后创建docker-compose.yml:
version: "3.5" services: anythingllm: image: mintplexlabs/anythingllm:latest container_name: anythingllm ports: - "3001:3001" environment: STORAGE_DIR: "/app/server/storage" volumes: - ./storage:/app/server/storage restart: unless-stopped启动容器:
docker compose up -d启动完成后,访问http://服务器IP:3001打开管理界面。第一次进入时会要求设置管理员账号。之后在模型接入配置中,你可以选择 Ollama、OpenAI 兼容接口、本地向量库等多种模式。
AnythingLLM 在防幻觉上的主要体现在三个设计:
第一,它会自动做文档切块和向量化。文档上传后不是原样丢给模型,而是按段落长度切成多个 chunk,然后转为向量索引。回答时只取与问题最相近的几块内容,而不是把整个知识库塞进上下文。
第二,Workspace 隔离非常实用。不同团队、不同项目维护自己的知识空间,减少无关内容干扰。
第三,支持“仅对话模式”和“知识库模式”切换。没有接入文档的纯聊天场景中,你可以配置成让它明确不要尝试回答业务问题。
如果你的团队对数据私密性要求不高,只是想验证效果,可以先用它默认的内部向量库。如果生产环境要求更高,可以把它对接外部的向量检索引擎,例如 Qdrant 或 Milvus。这个切换不会影响使用界面,因为 AnythingLLM 在设计中已经把向量库做成了可替换的后端。
4. 端侧小模型:14 MB 级模型不是大模型替代品,而是“门卫”
很多人看到“14 MB 端侧模型”后会有一个误解:以为大模型已经小到可以塞进几十 MB 了。这种理解要纠正一下。
所谓 14 MB 级别的模型,并不是一个能做开放域问答的 Chat 模型,而是一个承担窄任务的专用小模型。它可以完成意图识别、关键词抽取、敏感内容判断、问题分类等结构化任务。大模型是大脑,它是门卫。门卫的职责不是替大脑回答所有问题,而是决定“这个问题该不该进入大脑、以什么方式进入大脑”。
如果所有用户提问都直接发给大模型,模型很容易被带偏。比如用户随口问一句“你觉得我们公司的售后好不好”,这个主观问题如果进了企业知识库问答链路,模型要么硬编一个观点,要么满篇客套话。更好的做法是先用端侧模型识别这是“观点类问题”,直接走话术回复,不进入知识检索。
再比如,用户问“退货补偿金额是多少”。这个问题的核心实体是“退货补偿”,端侧模型可以先抽取实体和意图,后续检索模块再根据这个结构化结果去查库。由于答案的关键路径由程序控制,不再完全依赖模型自由发挥,幻觉风险自然会下降。
端侧模型通常使用 ONNX Runtime 部署。这里给出一个把 Hugging Face 模型导出为 ONNX 再量化的思路。第一步安装工具:
pip install optimum onnx onnxruntime导出一个小型 BERT 类模型:
optimum-cli export onnx --model prajjwal1/bert-tiny bert-tiny-onnx/然后使用 ONNX Runtime 的动态量化脚本把 FP32 模型压缩为 INT8 模型:
from onnxruntime.quantization import quantize_dynamic, QuantType model_fp32 = "./bert-tiny-onnx/model.onnx" model_int8 = "./bert-tiny-onnx/model-int8.onnx" quantize_dynamic( model_fp32, model_int8, weight_type=QuantType.QInt8, ) print("量化完成:", model_int8)量化完成后,模型文件大小通常会比原来的 FP32 版本小很多,像 bert-tiny 这类参数量很小的模型,压到 14 MB 甚至更小是完全可能的。
这里必须强调,文件大小不是唯一指标,你需要用自己的业务数据测试分类准确率。小模型的优势是推理快、可本地运行、不依赖网络;劣势是理解能力有限。它适合的是边界清晰的分类和抽取任务,而不是考试型问答。
实际生产环境中的调委会设计成:
def route_question(text: str) -> str: # 示意代码,需要加载自己的端侧模型 # 0: 闲聊 1: 知识库问题 2: 不适合回答 prediction = tiny_intent_model(text) if prediction == "small_talk": return "闲聊,不进入知识库" if prediction == "unsafe": return "拒绝回答" return "进入检索与生成链路"在多 Agent 架构中,这类端侧模型还可以用来做“结果校验”。大模型生成答案后,另一个小模型判断“最终回答中的关键断言是否都能在给定证据中找到”。如果找不到,就要求重写或者直接拒绝。这种“先生成后校验”的思路,是当前工程上抑制幻觉的重要手段。
5. 本地跑大模型:Ollama 让生成环节稳定可控
端侧小模型解决了一部分分流问题,但最终生成正式回答的环节,仍然需要一个能力强一些的大模型。在很多企业场景中,这个模型不能部署在公有云上,因为内部制度、客户信息、财务数据都不适合发送到外部接口。
Ollama 是目前最流行的开源本地模型运行时之一。它把模型下载、加载、推理封装成了类似 Docker 的命令方式,开发者可以快速在本地或内网拉起一个大模型服务。
安装 Ollama 在 Linux 上非常直接:
curl -fsSL https://ollama.com/install.sh | sh下载模型:
ollama pull qwen2.5:7b启动一个交互式对话:
ollama run qwen2.5:7b仅有一个原生模型还不够。更推荐为业务场景创建一个定制模型,把系统提示词固化在模型配置中。这种做法的好处是可以防止调用方忘记添加系统提示词,或者模型被 Prompt 注入干扰。
创建一个Modelfile:
FROM qwen2.5:7b SYSTEM """ 你是一个企业知识库助手。只能依据提供的上下文回答问题。 当上下文中没有明确信息时,必须回答:资料中没有找到相关内容。 禁止猜测业务规则,禁止编造金额、日期、政策条款。 """使用 Ollama 创建专属模型:
ollama create kb-agent -f ./Modelfile之后通过 HTTP API 调用:
curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{ "model": "kb-agent", "prompt": "用户问题:申请退款后多久到账?\\n上下文:略", "stream": false, "options": { "temperature": 0 } }'示例中的temperature: 0值得特别说明。温度参数控制生成随机性。温度越高,模型越可能尝试不同的表达方式;温度接近 0,模型输出会更稳定,更适合知识库问答。如果你正在做一个答案需要反复对比的系统,不要在生产环境调高温度。
使用本地模型还有一个连带优势:Prompt 内容和检索证据都停留在内网,数据安全边界更清晰。但要注意,本地部署不等于完全无风险。模型文件需要从外部下载,下载后要校验哈希;服务端口不要直接暴露到公网,否则任何人都可能调用你的模型接口消耗资源。
6. 检索引擎:让答案先有“出处”再进入上下文
模型本身不具备查文件的能力,所以需要在模型外层接一个检索引擎。检索的作用是从海量文档中找到与问题最相关的段落,把这些段落作为“证据”提交给模型。只有证据充足,模型的生成才不会脱离事实。
这里以 Meilisearch 为例。Meilisearch 是一个开源、轻量的全文检索引擎,中文支持较好,很适合快速搭建企业知识库。
用 Docker 启动一个实例:
docker run -d --name meili \ -p 7700:7700 \ -e MEILI_MASTER_KEY=dev_master_key \ getmeili/meilisearch:latest准备一批文档,比如docs.json:
[ { "id": "POL-1001", "title": "商品退换货政策", "content": "用户签收商品后 7 天内可以申请无理由退货。退款将在退货审核通过后 1 到 7 个工作日内原路返回。" }, { "id": "POL-1002", "title": "会员积分规则", "content": "用户每消费 1 元可以获得 1 积分,积分有效期为 12 个月。" } ]创建索引并导入文档:
curl -X