我们每天都会向大语言模型抛出大量问题:“帮我查一下航班”“总结一下这份财报”“这段代码为什么会报错”。模型的回答流畅、自信,甚至颇具条理。但如果你追问一句:它刚才给出的答案,在什么意义上算“真实”?这个问题往往会让许多依赖LLM做应用的开发者陷入沉默。
大语言模型并不直接访问世界,也不像数据库那样定位一行记录然后返回给你。它的工作机制是:根据你已经输入的文本,预测接下来最可能出现的token序列。换句话说,它生成的不是一份“查询结果”,而是一种语言的再生成。它和外部世界之间,隔着一层被压缩进参数的概率分布——这就是本文标题中“Indirected Reality”(间接现实)的含义。模型输出的内容,是建立在语言形态之上的间接现实,而不是对世界状态的一手描述。
这篇文章想讨论的不是哲学问题,而是工程问题。我的核心判断是:LLM回答问题时,产出的不是事实,而是文本;把文本变成可验证、可追溯、可安全执行的动作,才是LLM应用开发真正的分水岭。如果你正准备构建RAG应用、Agent系统或工具调用服务,这篇文章会帮你理解“真实性缺口”从哪来,以及如何用检索、工具、权限校验和模型选型来填补这个缺口。
1. 从“Indirected Reality”说起:模型给出的答案,不是记录,是重建
“Indirected Reality”可以直译为“间接的现实”。理解这个词,首先要纠正一个直觉:很多人以为LLM是一个巨大的知识库,你在里面提问,它像搜索引擎一样把答案取出来。错了。LLM内部没有一张“事实表”,也没有“世界状态快照”。它只有一个训练阶段学到的统计结构,一个把token序列映射到下一个token概率分布的神经网络。
当你问“北京到上海的高铁要多久”,模型并不是去查询12306数据库,而是根据训练语料中学到的文字共现规律,推断出“4个半小时”“5小时”“京沪高铁”这些词曾经以怎样的顺序出现。它给出的答案,是一种对“真实形式”的重建。大多数情况下,这种重建高度逼近事实,因为它从海量文本中学到了大量通用知识和推理模式;但它不具备任何机制来保证这一点。
Karpathy在社区中提出的“LLM wiki”范式之所以引起讨论,正是因为它在某种程度上回应了这个缺陷:把LLM当作知识沉淀和检索的接口,而不是把模型本身当作事实的来源。用户向模型提问,模型给出回答,但回答会被整理、校验、修订,并沉淀成一份可检索的知识条目。这个模式下,模型扮演的是“文本组织者”,而真实性的来源是那个被持续修订的wiki库。
对开发者来说,这带来一个非常具体的推导:你永远不应该把原始模型输出直接当成业务事实落库,也不应该让模型在没有外部证据支撑的情况下完成高风险判断。凡是要求“真实”的场景,都要在模型外围建立事实层。
2. 语言与真实之间的三层裂缝:语义、知识、指涉
为了把“真实性”问题拆解成可操作的技术问题,我建议把它分成三层来看。理解了这三层裂缝,你就知道为什么LLM会一本正经地胡说八道,也知道该在哪一层做介入。
第一层:语义裂缝。自然语言本身就存在歧义。同一个词在不同语境下含义不同,“苹果”可以指水果,也可以指公司。模型只能根据上下文猜测语义,没有传感器去体验世界。这一层几乎所有NLP系统都会遇到,LLM表现已经足够好,但并非完美。
第二层:知识裂缝。模型的知识来自于训练语料,它有两个天然边界:知识截止日期和语料覆盖范围。训练集中没有的新事件,模型无法“知道”;训练集中稀缺的领域,模型只能靠猜测填补。更麻烦的是,模型在不确定时不会停下来,它会基于语言惯性继续生成一个看起来合理的答案。这就是幻觉(hallucination)的机制根源。
第三层:指涉裂缝。LLM的输出本身无法指向外部对象。它可以生成包含“订单号ABC123”的文本,但这句话不能保证该订单在数据库中真实存在。当模型被接入API、数据库、命令行工具后,指涉关系变成由工具调用结果来建立;但在没有工具的场景下,模型输出的任何实体都只是文本符号,不具备与真实世界对象的绑定关系。
从这些裂缝可以看出一个趋势:LLM单靠“生成”无法逼近真实,必须通过“检索”和“动作”来补充外部信号。这也解释了为什么当前LLM应用的主流架构不是“模型直接回答问题”,而是“模型+外部工具+上下文管理”的组合。
3. 从理论到工程:开发者为什么必须关心真实性
如果你的LLM应用只是做一个聊天玩具,回答错了可以一笑而过。但当你把它接入客户服务、数据报表、代码生成、订单操作,真实性就是系统的基础诉求。一旦模型把不存在的订单信息当作事实输出,用户会投诉;一旦模型根据幻觉上下文调用删除接口,可能发生不可逆的故障。
在实际项目中,LLM应用的开发难度,往往不是模型本身,而是如何处理不确定性。以企业内部知识库问答为例:模型可能把去年A部门的制度套用到今年B部门的新流程上,因为它们的表达非常相似。没有检索过程限定上下文,模型就不知道“新制度已经发布,旧制度已被替代”。这直接导致回答错误。解决办法是把相关文档检索出来,让模型只基于这些文档作答。
再比如Agent场景。模型被赋予调用工具的权限后,“真实性”进一步变成“权限边界”问题。如果模型可以调用数据库写入接口,它可能基于一个错误判断执行危险操作。社区安全测试里专门有一种漏洞类型叫excessive agency,即模型被授予超出任务所需的工具权限。这类问题的根源正是:模型判断的是“说什么”,而系统必须决定“能做什么”。
因此我们可以给出一条工程原则:模型负责表达,系统负责事实与边界。所有的事实认定、权限判断、操作审批,都应该由模型外部的工程组件承担,而不是把希望寄托在模型“自己会小心”。
4. 实践中缓释不确定性的主流架构:RAG、工具调用与Agent
理解了“模型只负责文本生成”之后,再来看业界主流的LLM应用架构,就非常清晰了。所有的架构努力,本质上都是在弥补语言生成与真实世界之间的空隙。
4.1 RAG:给生成过程提供证据上下文
RAG(Retrieval-Augmented Generation,检索增强生成)是目前最基础的方案。它把“知识获取”从模型内部参数转移到外部检索系统。流程是:先把用户的查询拿出来,到向量数据库或文档库中检索相关片段,把检索到的片段拼进Prompt,再让模型基于这些片段生成回答。这样模型的输出就不是凭记忆和惯性,而是被限定在一个可控的“证据范围”内。
下面是最小化的RAG示意,帮助你理清结构:
# 文件路径:rag_quickstart.py from typing import List # 1. 一个极小的文档库,生产环境应使用向量数据库 DOCUMENTS = [ {"id": "d1", "text": "Apollo配置中心用于集中管理分布式系统配置。"}, {"id": "d2", "text": "大模型幻觉指模型生成与事实不符或缺乏依据的内容。"}, {"id": "d3", "text": "RAG通过检索外部知识片段,为生成过程补充证据。"}, ] def retrieve(query: str, top_k: int = 2) -> List[str]: """示意检索函数:按字面命中的粗略打分排序。 生产环境应替换为向量检索,例如使用 embedding 模型 将 query 和文档都转成向量,再做相似度排序。 """ scored = [] for doc in DOCUMENTS: score = sum(1 for ch in query if ch in doc["text"]) scored.append((score, doc["text"])) scored.sort(key=lambda x: x[0], reverse=True) return [text for _, text in scored[:top_k]] def build_prompt(query: str) -> str: contexts = "\n".join(f"- {t}" for t in retrieve(query)) return f"""请根据给定资料回答问题。 资料: {contexts} 问题:{query} 要求:只依据上述资料作答,资料中没有的信息请明确说明“资料未提及”。""" if __name__ == "__main__": q = "RAG能解决大模型幻觉问题吗?" print(build_prompt(q))这段代码的重点不是检索质量,而是思路:先给模型提供证据片段,再让它作答。如果检索结果为空,模型就应当回答“资料未提及”,而不是自己编造。这一步已经能有效降低幻觉率。
4.2 工具调用:把事实问题交给工具解决
有些问题靠检索解决不了,比如“我的订单现在到哪了”。订单状态是一个实时动态数据,必须在查询时调用订单服务。这就要使用工具调用(Function Calling / Tool Use)。模型负责理解用户意图、抽取参数,然后由系统去调用真实接口。
以订单查询为例,模型侧可以声明这样一个工具:
{ "name": "query_order_status", "description": "查询已登录用户自己的订单物流状态。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为 ORD 开头" } }, "required": ["order_id"] } }模型根据这个JSON Schema,决定是否调用工具、传入什么参数。系统收到工具调用请求后,需要先做校验,再执行真实查询。这里有一个关键工程点:工具调用结果必须被拼接回上下文中,成为模型后续回答的证据。模型不能凭空说“订单已发货”,它必须读到工具返回的状态字段。
# 文件路径:tool_executor.py def execute_tool(name: str, args: dict) -> dict: """工具执行器:只允许执行白名单内的工具。""" if name != "query_order_status": raise PermissionError(f"未授权工具: {name}") order_id = args.get("order_id", "") # 真实项目中,这里会调用订单服务并校验当前用户是否有权查看 return {"order_id": order_id, "status": "shipped"}工具调用的意义在于:把“真假判断”从模型的语言能力里剥离出来,交给业务系统去保证。
4.3 Agent与MCP:多步骤协作与统一接入
当任务复杂到需要多轮工具调用时,就进入了Agent的范畴。Agent是一个循环:理解任务 → 决定调用哪个工具 → 读取工具结果 → 决定下一步。每一步都可能涉及检索、计算、查询。
这种多步骤编排给开发带来的额外成本是工具接入复杂度。每个数据源或工具有不同的协议、认证方式和参数格式,Agent每接一个都要写一遍适配逻辑。于是社区开始推动MCP(Model Context Protocol,模型上下文协议),它的目标是标准化“模型到工具”的接入方式,可以理解为工具侧的通用接口规范。结合Spring AI等框架,开发者可以把RAG、MCP、Agent组合起来,形成一个相对完整的LLM应用骨架。
当然,MCP并不能解决真实性,它解决的是接入效率。真实的保障仍然来自上下文证据、权限校验和人工审批。
5. 从“文本生成”到“动作执行”的边界风险:过度授权与权限管控
当模型从“回答问题”升级为“操作工具”后,新的问题出现了:模型生成文本的能力,和系统执行动作的权限,必须被严格分离。安全测试中的“excessive agency”指的就是模型被授予了超出任务所需的权限,一旦它被提示词注入或错误推理诱导,就可能执行预料之外的操作。
举一个保守但典型的例子:一个客服Agent本应只查询订单状态,但如果不加限制地给它挂载了数据库写入工具和命令行工具,攻击者可能通过精心构造的对话让模型认为“用户要求删除测试数据库中的记录”,从而触发一个灾难性动作。模型本身没有恶意,但它对动作后果缺乏感知。
工程上遏制这一风险,通常从三方面入手。
第一,最小权限白名单。Agent只能调用任务必需的工具,且在配置层面明确禁止高风险操作。下面是一个示例结构:
# 文件路径:agent_policy.yaml # 字段以实际框架为准,这里展示的是通用权限设计思路 agent: max_steps: 10 allowed_tools: - call.search_web - call.query_database_readonly denied_tools: - call.delete_database - call.execute_shell human_approval_required: - call.write_file - call.send_email第二,高危操作必须人工审批。写文件、发邮件、删除数据这类操作,不应该由模型自动执行,而应进入审批流程。系统可以在模型发出工具调用请求后,先暂停,通知操作者确认,确认后才执行。这是最高效的兜底策略。
第三,沙箱与审计。在测试环境验证Agent行为,记录每一次模型推理、工具调用、返回结果的完整日志。一旦出现异常,可以根据日志追溯是哪一轮Prompt、哪一个工具调用导致了问题。
这条原则值得反复强调:永远不让模型独自决定高风险动作。你可以信任模型的说服能力,但不要信任它作为“执行者”的自我克制能力。
6. 支撑LLM可靠运行的细节:精度、上下文与模型选择
真实性并不仅仅由Prompt和架构决定,底层的推理精度也会影响生成质量。这也是“LLM大模型之精度问题(fp16、fp32、bf16)”这类话题始终被关注的原因。
在训练和推理中,模型权重和中间激活值通常使用低精度浮点数以节省显存、提升速度。常见选择包括:
| 精度 | 说明 | 适用场景 |
|---|---|---|
| fp32 | 单精度浮点,数值稳定,但显存占用高 | 小模型、调试、对比基线 |
| fp16 | 半精度浮点,显存占用减半,但有数值溢出风险 | 多数消费级显卡、一般推理 |
| bf16 | 扩展指数位的半精度,动态范围更接近fp32,更稳定 | A100/H100等新硬件上的大模型训练与推理 |
| int8/4bit | 量化方案,显存进一步下降,但可能带来精度损失 | 本地部署、消费级硬件跑大模型 |
在开发阶段,一个实用建议是:先用fp32或bf16跑通业务逻辑,确认模型输出符合预期,再做量化或低精度加速。不要在功能还没验证时就为了省显存把精度压得太低,否则会出现“模型本身能回答对,但低精度推理后频繁出错”的定位困境。
下面是基于HuggingFace transformers库的加载示意:
# 文件路径:load_model.py import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_id = "your-model-id" # 替换为实际使用的模型 tokenizer = AutoTokenizer.from_pretrained(model_id) # bf16 需要硬件支持;老显卡可改 fp16,显存不足可考虑 4bit 量化 model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.bfloat16, device_map="auto", )上下文策略同样重要。即便模型声称支持很长的上下文窗口,也并不意味着你要把所有内容全塞进去。更稳妥的做法是:先做检索/筛选,只把与当前问题相关的片段拼入上下文,并合理控制长度。长上下文中混杂大量无关信息时,模型反而更容易丢失关键事实。
模型选择层面,建议根据任务复杂度权衡:简单问答、信息抽取可以用小参数模型,成本低、速度快;多步推理、复杂工具调用,则需要更强的大模型,否则容易出现“工具调用了但参数传错”的低级错误。
7. 构建生产级LLM应用的工程清单
把前面的思路整理成一份可直接对照的工程清单,适用于大多数LLM应用场景。
输入侧:上下文组装。
- 先检索,后生成;没有证据,就不让模型自由发挥。
- 给模型明确的“不知道”出口,例如在Prompt里写“资料未提及时必须说明”。
- 对用户输入做基础检查和长度限制,防止上下文被异常数据撑爆。
- 注意提示词注入风险,对包含外部文本的内容标识来源。
输出侧:校验与引用。
- 对关键输出做格式校验,至少保证JSON、SQL、代码片段是合法的。
- 涉及数据查询时,尽量让工具返回结构化字段,再渲染给用户。
- 重要回答可以要求模型给出引用来源,便于人工复核。
- 对风险结论设置置信度判断或二次确认。
运行侧:日志与观测。
- 记录每一轮完整Prompt、模型原始输出、工具调用输入输出。
- 监控工具调用的失败率、超时率,这能反映模型的意图理解质量。
- 对模型输出的稳定性做测试,而不是只看一两次效果。
- 保存模型版本和Prompt版本,便于回滚定位。
安全与合规侧:权限与审计。
- 遵循最小权限原则;只给Agent任务必需的工具。
- 高风险操作必须人工确认,不允许自动执行。
- 涉及用户隐私数据时,先确认数据使用范围,不把敏感数据随意拼入外部模型调用。
- 对工具调用设置频率限制和操作审计。
这份清单的本质,是把模型从“回答者”重新定位为“提案者”。模型提出文本,系统决定是否呈现、是否执行、是否记录。对生产系统而言,后者才是可控的。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型回答与提供的资料明显矛盾 | 检索片段未进入Prompt,或上下文被截断 | 检查最终提交给模型的Prompt内容 | 确认检索结果被正确拼入上下文,检查长度截断逻辑 |
| 检索到了内容,但模型仍然编造新事实 | Prompt约束不够强 | 查看模型输出是否完全脱离上下文 | 在Prompt中明确要求“只依据资料”,并检测答案与资料的相似度 |
| 工具调用参数频繁出错 | 模型能力不足,或工具描述不清晰 | 查看模型生成的参数JSON | 改进工具描述和参数说明,或更换更强模型 |
| 低精度推理后效果明显下降 | 精度选择不合理 | 用高精度跑一次对比 | 换bf16或fp32,必要时量化校准 |
| 模型执行了未预期的工具操作 | 权限配置过宽 | 查看审计日志中的工具调用记录 | 收紧工具白名单,高危操作加人工审批 |
| Agent循环不终止,反复调用工具 | 最大步数未限制 | 查看调用链日志 | 设置max_steps,增加终止条件 |
这些问题的共性特征在于:大多数LLM应用故障,都不是模型“不会回答”,而是工程链路中某个环节没有对模型输出做约束和验证。排查时不要只盯着Prompt,按“输入Context → 模型输出 → 工具执行 → 最终响应”这条链路逐步梳理,定位会快很多。
9. 结语与下一步实践方向
“Indirected Reality”这个标题最终想表达的是:大语言模型并不直接生活在真实世界里,它只是被文本训练出来的、擅长生成文本的系统。能够流畅回答事实性问题,是它对文本规律学习的结果,而不是它连接了某个真实事实库。对普通用户来说,这种区别并不明显;但对开发者来说,它决定了整套系统架构的走向。
接受“模型输出只是语言”这个前提,反而能让应用更可靠。你会自然地给系统加上检索层,让模型有证据可依;你会在工具调用前加权限校验,避免模型越权;你会在Prompt里明确“不知道就说不知道”,减少一本正经的胡说八道;你会在上线前做回归测试,而不是被一两次惊艳的Demo冲昏头脑。
下一步,建议从最简单的RAG流程开始,把“检索 + 生成”跑通,然后给系统挂一个工具调用,加上白名单和日志审计。当你亲手把一条模型输出变成经过验证、可追溯的业务结果时,你就真正理解了LLM应用的工程核心。如果你正在规划Agent系统,请把权限边界设计放在功能开发之前——它会帮你省下大量事故后的排查时间。