
1. 从一条假情报说起AI幻觉为什么能骗过情报分析系统2024年底到2025年初圈子里流传过一个让人后背发凉的故事某情报分析系统在整合多源信息时因为大语言模型LLM产生幻觉把一段虚构的“船只动向”当成了真实情报差点触发一次海上拦截行动。虽然最终被人工复核拦下但这件事在OSINT开源情报和SIGINT信号情报圈子里炸开了锅——大家突然意识到AI幻觉不再是聊天机器人胡说八道那么简单它已经能渗透进高 stakes 的决策链条。我做了七八年数据分析和情报系统相关的工作早期用传统NLP做实体抽取和关系图谱近三年全面转向LLM和RAG检索增强生成架构。说实话第一次听到这个案例时我并不意外。因为在我自己搭建的多个OSINT聚合系统里LLM编造细节、混淆时间线、把不同来源的信息缝合在一起的情况几乎每周都会遇到。区别只在于我的系统后面有人工复核兜底而那个案例里的系统自动化程度太高了。这篇文章不打算复述那个具体事件——涉及的信息太敏感也没必要。我想做的是把这个案例背后真正值得技术人关注的东西拆开AI幻觉在情报类RAG系统中到底怎么产生的为什么传统的“检索生成”架构挡不住它以及如果你正在搭建类似的OSINT/SIGINT分析系统有哪些实操层面的防御手段可以落地。适合读这篇的人正在做RAG项目、对LLM可靠性有要求的开发者做OSINT聚合、舆情分析、威胁情报的技术负责人以及任何想把LLM接入生产决策链路的工程师。我会尽量少讲空泛的“AI伦理”多讲代码层面、架构层面、Prompt层面的具体做法。2. 幻觉不是bug是LLM的“出厂设置”2.1 为什么LLM会“一本正经地胡说八道”要理解AI幻觉得先接受一个反直觉的事实LLM的本质是概率续写机器不是知识数据库。它做的事情是根据上文预测下一个token最可能是什么。当你问它“某艘船现在在哪”它并不是在数据库里查询而是在计算“在‘某艘船现在在’这个上下文后面哪个地点词出现的概率最高”。这就解释了为什么幻觉往往出现在这些场景训练数据里没有的信息模型没见过但它不会说“我不知道”而是根据语言模式编一个最像答案的答案。时间敏感的信息模型的知识有截止日期问它最近的事件它要么拒答要么用旧信息拼凑。需要精确数字的场景坐标、吨位、航速、时间戳这些一旦编错后果比编错一个形容词严重得多。我实测过一个典型的例子给模型一段关于某海域船只活动的模糊描述问它“这艘船的目的地是哪里”。模型会非常自信地给出一个具体港口名甚至配上经纬度。但如果你追问“这个信息来自哪段原文”它就开始含糊其辞。幻觉的可怕之处不在于错而在于错得极其自信、极其具体。2.2 情报场景下幻觉的破坏力被放大了普通聊天场景里幻觉顶多让你得到一个错误答案。但在OSINT/SIGINT场景里幻觉会沿着一条链路放大信息聚合阶段LLM从多个来源抽取实体和事件把A来源的船名和B来源的时间、C来源的地点缝合在一起生成一条看似完整的新情报。交叉验证阶段如果系统用另一个LLM去“验证”这条情报而验证模型又基于同样的幻觉逻辑就会形成“幻觉共振”——两个模型互相确认一个不存在的事实。决策输出阶段这条被“验证”过的假情报进入分析报告人类分析师看到的是结构清晰、来源标注完整的内容很容易降低警惕。那个险些引发拦截的案例大概率就是这条链路走通了。单点幻觉不可怕可怕的是系统架构让幻觉有了“合法身份”。2.3 RAG不是万能药检索增强的边界在哪很多人以为上了RAG就能解决幻觉。RAG的思路确实对先检索真实文档再让LLM基于检索结果生成答案相当于给模型开卷考试。但实操下来RAG只能缓解幻觉不能消除幻觉原因有几个检索本身会失败如果知识库里没有相关信息检索器返回的是“最相似”的文档而不是“正确”的文档。LLM拿到不相关的上下文照样能编。LLM会忽略检索结果当检索内容和模型内部知识冲突时模型有时会选择相信自己“记住”的东西。这在需要精确匹配的场景里是致命的。多跳推理会引入新幻觉RAG系统经常需要多轮检索和推理每一轮都可能引入偏差误差累积。我在一个船舶轨迹分析项目里做过统计纯LLM抽取实体准确率大概在70%左右加上RAG后提升到85%但剩下的15%错误里有相当一部分是“检索到了正确文档但LLM生成时篡改了细节”。RAG解决的是“有没有依据”的问题没解决“依据用没用对”的问题。3. 拆解情报类RAG系统的核心架构与风险点3.1 一个典型的OSINT/SIGINT RAG流水线长什么样先把这个案例背后可能的技术栈还原一下。一个完整的情报分析RAG系统通常包含这几层层级功能常用技术幻觉风险点数据采集层抓取公开信息、信号数据、卫星AIS等爬虫、API对接、流处理数据源本身不可靠预处理层清洗、去重、实体抽取规则引擎、NER模型抽取错误被下游放大索引层切块、向量化、建索引嵌入模型、向量数据库切块不当导致上下文丢失检索层语义检索、关键词检索、混合检索BM25、向量相似度、重排序检索到不相关文档生成层基于检索结果生成答案LLM、Prompt工程幻觉、篡改、缝合验证层事实核查、交叉验证规则、另一个LLM、人工验证模型本身有幻觉那个出事的系统问题很可能出在生成层和验证层的衔接上。生成层产出了一条缝合了多个来源的“新情报”验证层没有能力识别这种缝合反而因为格式规整、来源标注齐全而放行。3.2 检索层切块策略决定了幻觉的“原材料”质量RAG系统的第一道防线是检索。但很多人把精力花在选向量数据库上忽略了更基础的问题文档怎么切块。情报类文档有个特点信息密度不均匀。一份船舶动态报告里可能前80%是背景描述后20%才是关键的时间、位置、航向。如果你按固定长度切块关键信息可能被切散或者和无关内容混在一起。我试过几种切块策略实测下来比较稳的是语义切块重叠窗口# 基于语义相似度的切块示例简化版 from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap128, # 重叠128字符防止关键信息被切断 separators[\n\n, \n, 。, , , ], length_functionlen, )但语义切块也不是银弹。对于结构化程度高的情报数据比如AIS报文更好的做法是按事件切块一条船的一次位置更新作为一个chunk保留时间戳、坐标、船名等元数据。这样检索时可以用元数据过滤减少不相关文档的干扰。注意切块大小没有万能值。512 token适合大多数场景但如果你的文档里关键信息很分散可以试试256如果文档逻辑连贯性强1024也可以。关键是做A/B测试看哪种切法在检索准确率上表现最好。3.3 生成层Prompt里必须写死的几条规则生成层是幻觉的重灾区。我的经验是不要指望模型“自觉”不编造要在Prompt里用硬规则约束它。以下是我在情报类RAG系统里常用的Prompt模板核心部分你是一个情报分析助手。你的任务是基于提供的【检索结果】回答问题。 规则 1. 只使用【检索结果】中的信息不得引入任何外部知识。 2. 如果【检索结果】中没有足够信息回答问题直接回答“根据现有资料无法确认”。 3. 每个事实性陈述后面必须标注来源编号格式为[来源X]。 4. 不得对信息进行推测、补全或缝合。如果不同来源信息冲突分别列出并标注冲突。 5. 涉及时间、坐标、数量等精确信息时必须原文引用不得改写。 【检索结果】 {context} 【问题】 {question}这几条规则里第3条和第4条最关键。要求标注来源能让下游验证层快速定位信息出处禁止缝合能防止模型把不同来源的信息拼成一条假情报。但Prompt不是万能的。我实测发现即使写了“不得引入外部知识”模型在遇到检索结果模糊时还是有概率“脑补”。这时候需要输出格式约束来兜底。3.4 验证层用规则引擎给LLM上枷锁验证层是最后一道防线。我的做法是规则引擎LLM复核人工抽检三层。规则引擎负责硬性检查输出中的每个实体船名、地名、时间是否在检索结果中出现过如果出现是否标注了正确的来源编号时间线是否自洽比如“到达时间”不能早于“出发时间”。坐标是否在合理范围内这些检查用简单的字符串匹配和正则就能做成本低、速度快。任何一条不通过直接打回生成层重新生成或者标记为“待人工复核”。LLM复核负责软性检查用另一个模型最好是不同厂商的去判断“生成内容是否忠实于检索结果”。但这里有个坑复核模型本身也可能有幻觉。所以复核结果只能作为参考不能作为最终决策依据。人工抽检是底线。对于高 stakes 的情报输出必须保留人工复核环节。那个出事的案例如果人工复核环节没有被绕过大概率不会走到拦截那一步。4. 实操搭建一个抗幻觉的OSINT分析原型4.1 环境准备与工具选型这一节我带你搭一个最小可用的抗幻觉OSINT分析原型。技术栈选择如下LLM任何支持结构化输出的模型都可以我用的是本地部署的开源模型避免数据外泄。向量数据库Chroma或Qdrant轻量、易部署。编排框架LangChain或LlamaIndex我选LangChain生态成熟。嵌入模型BGE-M3或类似的多语言模型对中英文混合文档友好。验证层Python规则引擎第二个LLM实例。安装依赖pip install langchain langchain-community chromadb sentence-transformers pip install pydantic fastapi uvicorn提示如果你的数据涉及敏感信息强烈建议本地部署LLM和嵌入模型。API调用虽然方便但数据出域的风险在情报场景里不可接受。4.2 数据预处理把非结构化情报变成可检索的块假设你有一批OSINT报告格式是纯文本或Markdown。预处理的目标是切成带元数据的chunk。import hashlib from datetime import datetime from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.schema import Document def preprocess_intel_report(raw_text: str, source_name: str, report_date: str): 将一份情报报告切块并附加元数据 splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap128, separators[\n\n, \n, 。, , , ], ) chunks splitter.split_text(raw_text) documents [] for i, chunk in enumerate(chunks): # 为每个chunk生成唯一ID chunk_id hashlib.md5(f{source_name}_{i}_{chunk[:50]}.encode()).hexdigest()[:12] doc Document( page_contentchunk, metadata{ source: source_name, report_date: report_date, chunk_index: i, chunk_id: chunk_id, ingested_at: datetime.now().isoformat(), } ) documents.append(doc) return documents这里的关键是元数据要足够丰富。source、report_date、chunk_id这三个字段在后续检索和验证时都会用到。特别是chunk_id它是追溯信息出处的唯一标识。4.3 检索层实现混合检索重排序单一向量检索在情报场景下不够用因为很多关键信息是精确匹配船名、编号、坐标。我通常用BM25向量检索重排序的三段式。from langchain.retrievers import BM25Retriever, EnsembleRetriever from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings def build_hybrid_retriever(documents, persist_dir./chroma_db): 构建混合检索器BM25 向量检索 # 向量检索 embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-m3) vectorstore Chroma.from_documents( documentsdocuments, embeddingembeddings, persist_directorypersist_dir, ) vector_retriever vectorstore.as_retriever( search_kwargs{k: 10} ) # BM25检索 bm25_retriever BM25Retriever.from_documents(documents) bm25_retriever.k 10 # 集成检索器权重可调 ensemble_retriever EnsembleRetriever( retrievers[bm25_retriever, vector_retriever], weights[0.4, 0.6], # BM25权重稍低向量权重稍高 ) return ensemble_retriever权重设置是个经验活。如果你的查询里经常包含精确的船名、编号BM25权重可以调到0.5甚至更高如果查询偏语义化向量权重调高。我一般从0.4/0.6开始根据实际效果微调。检索到文档后还需要重排序。重排序模型如BGE-Reranker能显著提升Top-K的准确率。这一步不能省因为检索器返回的Top-10里可能只有前3个是真正相关的。4.4 生成层实现带来源标注的受控生成生成层的核心是强制模型标注来源。我用Pydantic定义输出结构让模型按格式返回。from pydantic import BaseModel, Field from typing import List, Optional from langchain.output_parsers import PydanticOutputParser class IntelStatement(BaseModel): 一条情报陈述 content: str Field(description情报内容必须忠实于检索结果) source_ids: List[str] Field(description来源chunk_id列表) confidence: str Field(description置信度high/medium/low) conflicts: Optional[str] Field( defaultNone, description如果不同来源冲突在此说明 ) class IntelReport(BaseModel): 完整的情报分析输出 statements: List[IntelStatement] Field(description情报陈述列表) unable_to_confirm: List[str] Field( default_factorylist, description无法确认的问题列表 ) parser PydanticOutputParser(pydantic_objectIntelReport) INTEL_PROMPT 你是一个情报分析助手。基于以下检索结果回答问题。 {format_instructions} 规则 1. 只使用检索结果中的信息不得引入外部知识。 2. 每条陈述必须标注来源chunk_id。 3. 如果检索结果不足以回答问题在unable_to_confirm中列出。 4. 不得推测、补全或缝合不同来源的信息。 5. 时间、坐标、数量等精确信息必须原文引用。 检索结果 {context} 问题{question} 用Pydantic约束输出格式的好处是解析失败可以直接触发重试。如果模型返回的JSON不符合结构或者source_ids里出现了检索结果中不存在的chunk_id系统可以自动打回重生成。4.5 验证层实现规则引擎兜底验证层用纯Python实现不依赖LLM保证确定性。import re from typing import List, Dict class IntelValidator: 情报输出验证器 def __init__(self, retrieved_chunks: List[Dict]): # retrieved_chunks: [{chunk_id: ..., content: ...}] self.chunk_map {c[chunk_id]: c[content] for c in retrieved_chunks} self.all_content .join(self.chunk_map.values()) def validate_source_ids(self, report) - List[str]: 检查所有source_ids是否真实存在 errors [] for stmt in report.statements: for sid in stmt.source_ids: if sid not in self.chunk_map: errors.append(f来源ID不存在: {sid}) return errors def validate_entities(self, report) - List[str]: 检查陈述中的关键实体是否在来源中出现 errors [] # 提取船名、地名等实体简化版实际可用NER模型 entity_pattern re.compile(r[\u4e00-\u9fa5]{2,10}(?:号|舰|船|港|岛|礁)) for stmt in report.statements: entities entity_pattern.findall(stmt.content) for entity in entities: if entity not in self.all_content: errors.append( f实体{entity}未在检索结果中出现疑似幻觉 ) return errors def validate_time_consistency(self, report) - List[str]: 检查时间线是否自洽 errors [] # 提取时间戳简化版 time_pattern re.compile(r\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}) for stmt in report.statements: times time_pattern.findall(stmt.content) if len(times) 2: # 检查时间顺序是否合理 parsed sorted(times) if parsed ! times: # 这里只是示例实际逻辑要根据业务规则 pass return errors def validate_all(self, report) - Dict: 执行所有验证 return { source_id_errors: self.validate_source_ids(report), entity_errors: self.validate_entities(report), time_errors: self.validate_time_consistency(report), }这个验证器能拦住大部分低级幻觉编造不存在的来源、引入检索结果里没有的实体、时间线矛盾。对于更隐蔽的“缝合式幻觉”每个实体都真实存在但组合方式是错的规则引擎无能为力需要人工复核或更复杂的交叉验证。4.6 完整流程串联把上面几层串起来def analyze_intel_query(query: str, retriever, llm, max_retries3): 完整的情报分析流程 # 1. 检索 retrieved_docs retriever.get_relevant_documents(query) retrieved_chunks [ {chunk_id: d.metadata[chunk_id], content: d.page_content} for d in retrieved_docs ] # 2. 构建上下文 context \n\n.join([ f[来源{c[chunk_id]}]\n{c[content]} for c in retrieved_chunks ]) # 3. 生成带重试 for attempt in range(max_retries): try: prompt INTEL_PROMPT.format( format_instructionsparser.get_format_instructions(), contextcontext, questionquery, ) response llm.invoke(prompt) report parser.parse(response.content) # 4. 验证 validator IntelValidator(retrieved_chunks) validation_result validator.validate_all(report) has_errors any(validation_result.values()) if not has_errors: return { status: success, report: report, validation: validation_result, } else: # 有错误记录并重试 print(f第{attempt1}次验证失败: {validation_result}) except Exception as e: print(f第{attempt1}次生成失败: {e}) # 重试耗尽标记为待人工复核 return { status: needs_human_review, report: report if report in locals() else None, validation: validation_result if validation_result in locals() else None, }这个流程的核心思想是自动化能处理的自动处理处理不了的明确标记出来绝不强行输出。那个出事的系统很可能就是缺少了“标记待复核”这一步让有问题的输出直接流到了决策层。5. 常见问题与排查技巧实录5.1 检索到了正确文档但LLM还是编怎么办这是最让人头疼的情况。检索结果明明包含正确答案模型却生成了一个错误版本。我遇到过几次排查下来通常是这几个原因原因一上下文太长关键信息被淹没。如果检索返回了10个chunk每个512 token总共5000 token的上下文模型很容易忽略中间部分的信息。解决办法是减少检索数量提高精度。Top-3通常比Top-10效果好前提是重排序做得好。原因二Prompt里的规则太多模型顾此失彼。我试过在一个Prompt里塞十几条规则结果模型反而更容易出错。后来精简到5条核心规则效果明显提升。规则不在多在于每条都能被模型理解和执行。原因三模型本身的能力边界。有些开源模型在长上下文和指令遵循上确实弱一些。如果条件允许换一个在指令遵循上表现更好的模型或者用更大的参数版本。排查技巧把检索结果和模型输出并排打印出来逐句对比。如果模型输出里的某个事实在检索结果中找不到对应那就是幻觉如果找得到但被改写了那就是指令遵循问题。5.2 多轮对话场景下幻觉会累积RAG多轮对话有个隐蔽的坑上一轮的幻觉会污染下一轮的上下文。比如第一轮模型编了一个错误的时间第二轮用户追问“这个时间准确吗”模型会基于自己编的时间继续编。我的做法是每轮对话都重新检索不把历史对话直接塞进上下文。历史对话只用来理解指代和意图事实性内容一律从检索结果重新获取。def multi_turn_query(history, current_query, retriever, llm): 多轮对话历史只用于理解意图事实从检索获取 # 用历史对话改写当前查询补全指代 rewritten_query rewrite_query_with_history(history, current_query) # 用改写后的查询重新检索 retrieved_docs retriever.get_relevant_documents(rewritten_query) # 生成时只使用检索结果不使用历史对话中的事实 # ...5.3 问题速查表问题现象可能原因排查方法解决思路模型编造不存在的来源Prompt未强制标注来源检查输出中的source_id用Pydantic约束输出结构检索到正确文档但输出错误上下文过长/规则过多对比检索结果和输出减少Top-K精简Prompt多轮对话幻觉累积历史对话污染上下文检查每轮检索结果每轮重新检索历史只用于改写查询时间/坐标被改写模型“顺手”润色精确匹配原文Prompt中强调原文引用不同来源信息被缝合模型自动“整合”检查来源标注禁止缝合冲突分别列出验证模型也产生幻觉验证模型与生成模型同源换一个模型验证规则引擎人工复核兜底5.4 几个踩过的坑坑一以为温度调低就能消除幻觉。温度调低确实能减少随机性但不能消除幻觉。幻觉的根源是模型在“不知道”时选择编造而不是随机性。我试过temperature0模型照样编。坑二以为换更大的模型就能解决。大模型在知识广度上更好但在指令遵循和事实一致性上不一定比小模型强。我实测过几个不同规模的模型在“是否忠实于检索结果”这个指标上差异没有想象中大。坑三忽略了嵌入模型的语言匹配。如果你的文档是中英文混合嵌入模型必须支持多语言。用纯英文嵌入模型处理中文文档检索准确率会大幅下降间接导致幻觉增加。坑四没有做检索结果的去重。如果知识库里有重复内容检索会返回多个相似chunk浪费上下文窗口还容易让模型混淆。预处理阶段一定要做去重。6. 从架构层面降低幻觉风险几个值得尝试的方向6.1 Agentic RAG让模型自己决定检索什么传统RAG是“一次检索一次生成”。Agentic RAG的思路是让模型自己判断当前信息够不够需不需要再检索检索什么关键词这个思路在情报分析场景下特别有用因为情报查询往往是多跳的。比如“某船最近一次停靠的港口所在国家最近有什么海事活动”需要先查船的位置再查港口再查海事活动。传统RAG一次检索搞不定Agentic RAG可以分步检索。但Agentic RAG也引入了新风险模型可能选择错误的检索路径或者在多步推理中累积幻觉。我的建议是在每一步都加验证不要让模型“一口气”跑完多步。6.2 知识图谱RAG用结构化约束非结构化纯文本RAG的弱点在于信息之间的关系是隐式的。知识图谱可以把实体和关系显式化检索时不仅返回文本还返回实体关系路径。比如从文本中抽取出“船A—停靠—港口B—位于—国家C”这样的三元组存入图数据库。查询时先用图查询找到相关实体再用文本RAG补充细节。这样能大幅减少“缝合式幻觉”因为实体关系是结构化存储的不依赖模型生成。6.3 多模型交叉验证用不同模型互相“挑刺”用一个模型生成用另一个不同厂商、不同架构的模型验证。两个模型同时产生相同幻觉的概率比单个模型低得多。但要注意验证模型不能和生成模型共享相同的检索结果和Prompt否则会形成“幻觉共振”。验证模型应该独立检索、独立生成然后对比两者的输出差异。6.4 人在回路高 stakes 场景的最终防线不管技术多先进高 stakes 的情报决策必须保留人工复核。那个出事的案例如果人工复核环节没有被自动化流程绕过大概率不会走到拦截那一步。人工复核不一定要逐条检查可以设计成异常触发式系统自动标记低置信度、来源冲突、实体未匹配的输出人工只复核这些异常项。这样既保证了安全又不会让人工成为瓶颈。7. 写在最后几个我反复验证过的原则做情报类RAG系统这几年踩过的坑比写过的代码还多。如果只能记住几条原则我会选这几条第一永远不要相信LLM的“自信”。模型输出越具体、越流畅越要警惕。具体和流畅不等于准确在情报场景里模糊但诚实比精确但编造有价值得多。第二验证层的成本远低于幻觉的代价。多写几百行验证代码多跑几个规则检查比起一条假情报引发的后果成本可以忽略不计。第三自动化程度越高人工复核越要保留。全自动流水线看起来很酷但在高 stakes 场景里一个“待人工复核”的标记可能比一百行自动化代码更有价值。第四RAG是手段不是目的。不要为了用RAG而用RAG。如果你的场景里精确匹配就能解决问题用规则引擎可能比LLM更可靠。最后分享一个我最近在试的小技巧在Prompt里加一句“如果你不确定请回答‘不确定’这不会被视为失败”。实测下来模型说“不确定”的频率明显提高而编造的情况减少了。让模型知道“承认不知道”是被允许的比逼它“必须回答”要安全得多。