ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型三层架构拆解:输入侧与输出侧的工程实践

大模型三层架构拆解:输入侧与输出侧的工程实践

1. 从"中间那层不可动"说起:三层架构的分工逻辑

1.1 三层到底指哪三层

我拆过不少大模型项目,从最简单的聊天机器人到带RAG的文档助手,再到跑工具调用的Agent,最后发现一个特别朴素的规律:不管前端炫成什么样,模型服务的内部永远只有三层。

第一层是输入层。模型本质上只认识一串离散的Token,它看不到你的PDF、数据库、用户界面,也感知不到你要它干什么。输入层干的事,就是把你提供的原始信息,从文件、文本、检索结果、图片、语音这些形态,加工成模型能理解的一串Token序列。很多人以为输入层就是"把prompt塞给模型",太窄了。实际上RAG、系统提示词、上下文工程、多模态编码、Agent里的工具执行结果回填,全都是输入层的活,只是加工深度不一样。

第二层是核心计算层。这是预训练阶段练出来的那一坨参数矩阵,配合Transformer、注意力机制、KV Cache这些机制,对输入Token做逐层计算,输出下一个Token的概率分布。这一层说白了就是模型能力的底座,你问什么它能答什么、懂多少知识、推理水平多高,全看这一层。但这是最重的一层,预训练成本高到普通人根本碰不了,推理时也不可能在线改权重。

第三层是输出层。模型计算层输出的东西,本质上只是"下一个Token的概率分布",是一堆数字,不是一个能直接在页面上展示的回答。输出层干的事,就是从概率分布里采样出Token,再把Token拼成文本、JSON、工具调用指令、图片描述,最后送到应用侧去做校验、渲染、解析、执行。

这三层的边界不用背概念,你就记住一句话:输入层解决"模型能不能理解你要什么",计算层解决"模型本身懂不懂",输出层解决"模型的回答能不能被系统用起来"。

1.2 为什么产品创新几乎都发生在输入和输出两侧

这两年AI圈子的热闹,看起来每天都在出新东西。今天有人说RAG救了大模型幻觉,明天有人说Agent才是未来,后天又冒出来MCP这种工具协议。但如果你把每个新玩法往这三层框架里一放,会发现一个很反直觉的事实:几乎没有一个创新发生在中间的计算层。

RAG改的是输入层,它决定"送进上下文窗口的材料是什么";Function Calling改的是输出层,它让模型从"输出一段文字"变成"输出一个可执行的动作指令";流式输出改的是输出层的传输形态;提示词工程改的还是输入层;Agent宏观上看是输入输出两层组成的循环回路。中间那层呢?你还是用那个模型,参数没变、注意力机制没变、知识上限也没变。

这不是巧合,而是成本结构决定的。中间层是沉没成本最高的地方,一次预训练或者全参数微调,要烧掉大量的算力和数据,还得冒着模型能力倒退的风险。产品团队能做的,就是在输入层想尽办法把任务包装得更精准、给更多上下文材料,在输出层想尽办法把模型的原始输出转化成可用的系统行为。这就是为什么同一个模型,在A团队手里是客服机器人,在B团队手里是代码审查工具,在C团队手里是数据分析Agent。模型一样,三层架构没变,变的只有输入和输出侧的加工方式。

记住这个框架最大的价值在于:以后看到任何一个AI项目,你都不会再被花里胡哨的术语绕晕,可以直接问一句"它的输入层做了什么,输出层做了什么"。

2. 输入侧不是花活,而是"把问题翻译成Token"的艺术

2.1 RAG:把数据库塞进输入上下文

RAG是输入层改造里最典型的例子。它的名字叫"检索增强生成",听起来很高端,本质上就是在模型开口之前,先帮它把相关的资料查好,作为上下文的一部分喂进去。

实操上RAG分两步。第一步是离线索引:把你的知识库文档切块,每块做向量化,存进向量数据库。第二步是在线查询:用户问一个问题,先用同一个embedding模型把问题转成向量,去向量库里做相似度检索,取Top-K个片段,然后把"用户问题 + 检索出来的片段 + 系统提示词"拼合成一个完整的输入序列,交给模型。

这里有个关键点,很多人会忽略:RAG的效果好与坏,其实取决于输入层把检索材料整理得够不够干净。我见过不少项目,向量库检索回来的片段互相对不上,甚至有两个片段内容矛盾,模型面对这样的输入,输出自然就乱来了。所以做RAG不能只盯着"要不要用RAG",要盯着"检索回来的内容值不值得被当作上下文"。

我在实际项目里总结了三个输入侧经验:

  • 切块粒度不要盲目追求固定字数,按语义边界切,比如标题、段落、表格说清楚一块。
  • 检索结果里一定要带上文档标题、来源、页码这些元信息,模型才能判断该信谁。
  • 如果检索回来的片段质量参差不齐,宁可只塞两三条高置信度的,也不要塞满整个上下文窗口。

为什么RAG能有效?因为它把模型从"闭卷考试"变成了"开卷考试"。模型参数里写死的知识,可能过时、可能记错;但你把准确资料摊在它面前,它做的工作就从"回忆"变成了"摘抄和组织"。开卷考试永远比闭卷考试稳定,这是输入层优化最核心的底层逻辑。

2.2 提示词工程与上下文工程:先管理好输入端的信息秩序

提示词工程和上下文工程,这两个词经常被混着用,但它们的着力点其实不同。

提示词工程管的是指令。系统提示词里写"你是一个严谨的客服,回答不得超过200字",这是告诉模型用什么身份、什么语气、什么规则来回答。它解决的是"怎么说话"的问题。

上下文工程管的是材料。你给模型塞一份长达几十页的日志、一个交互过的历史记录、一串用户画像,这些都是上下文。它解决的是"跟谁说话、基于什么说话"的问题。

我见过太多团队,系统提示词写得像宪法一样长,把各种规则、few-shot示例、格式要求全堆进去,看起来是下了功夫。结果模型一上线就出问题。问题不在提示词写得不好,而是提示词本身也是输入Token,它会和上下文里的业务文本互相抢占注意力。尤其是上下文窗口长度有限的时候,你把5000字的规则塞进去,剩下的空间就没多少了,模型还容易把规则里的琐碎细节误解成业务要求。

实际操作中,我的习惯是:系统提示词控制在800字以内,只保留角色、任务边界、输出格式这三类信息。业务知识、检索资料、历史对话,放到用户消息侧去管理。两边分工明确,输入侧的信息秩序就清楚了。

有人会问,上下文越长不是越好吗?不是。上下文窗口有一个被低估的问题叫Lost in the Middle,模型对开头和结尾的Token关注度高,但对中间夹着的大量内容会视而不见。这意味着你把关键材料埋在几千Token的中间地带,模型大概率会漏看。所以关键信息要么放在系统提示词末尾,要么放在离用户问题最近的位置,要么用明显的分隔符标出来。

2.3 Agent:把一个大任务拆成一连串的输入输出循环

Agent是过去一年被讨论最多的概念,放在三层框架里看,它其实是输入层和输出层组成的一个循环机制。

传统问答是单向的:用户问一句话,模型回一段文字,结束。Agent则不一样,它可以多轮调用工具。用户说"帮我查一下这个订单的物流状态,然后写一封催发货的邮件",这时候模型先在输出层生成一个"调用查询物流接口"的指令,系统拿到这个指令去执行接口,再把接口返回的JSON结果作为新一轮的输入Token放回上下文,模型看到物流数据后,再次在输出层生成"根据这些信息起草邮件"的最终回答。

你会发现,Agent里的每一次工具调用结果,本质上都是"输入层的材料"。模型并没有变聪明,它只是在多轮循环里不停地看到新的上下文材料,每一次看到的材料都是系统从外部世界帮它捞回来的。这就是为什么很多人问"Agent和大模型推理有什么区别"时,我会说:Agent不是新的推理模型,它是一套输入输出管理机制,让你能用同一个模型连续处理需要外部信息的任务。

我在写Agent循环的时候,会在输入层刻意把"工具返回结果"放在一个结构化的XML块或者JSON块里,并在提示词里明确告诉模型"下面是被调用工具返回的原始数据,不是用户说的内容"。这个小细节能大幅减少模型把工具结果误当成用户指令的问题。

输入侧的加工手段也远不止这些。多模态输入(图片、音频先要转成视觉、语音编码)是输入层,向量检索是输入层,就连"把用户的历史对话截断到最近10轮"也是一种输入层裁剪策略。随便一个功能,你只要能说清楚"输入侧做了哪些处理",基本就抓住了项目的一半。

2.4 输入侧的常见翻车点

输入侧出了问题,表现是模型"答非所问"或者"没有按规则做事"。我列几个踩过的大坑:

第一,上下文污染。比如用户历史对话里混进了一段系统报错信息,模型分不清这是日志记录还是需要回答的内容,跟着跑偏。解决办法是给不同类型的上下文加明确标记,比如用<system_log>这种标签包起来,让模型一眼认出这是外部数据。

第二,无效上下文占用窗口。业务里很常见的是把整个报表或者一整个数据库表结构全塞进去,模型根本用不到,反而把有用的信息挤出了注意力范围。解决思路是先做字段级裁剪,只给模型跟当前意图相关的字段。

第三,检索结果的可信度问题。RAG的时候,检索出来的片段可能来自权威资料,也可能来自某篇质量很差的博客,模型默认一视同仁。最好在输入侧把来源等级标注给模型,比如"以下内容来自公司官方文档,优先级最高",让模型心里有杆秤。

第四,多轮对话里的身份错乱。用户问一嘴天气,又转去说业务需求,模型可能把之前的天气内容也当成业务上下文。输入侧可以做意图分类,分完再决定当前轮次向模型提供哪些历史上下文。

输入层的核心原则,用一句话概括:不干净的信息不要给模型,重要的信息不要埋在中间,该标注身份的必须标清楚。

3. 输出侧的核心命题:从"一句话"到"一个可被接管的系统"

3.1 结构化输出与JSON Mode

很多人低估了输出层的复杂性。模型输出的是概率分布,它并不知道你的系统需要什么。如果你的下游逻辑要拿模型的回答去更新数据库、调接口、渲染图表,你首先需要的是一个稳定、可解析的结构化输出。

最常用的手段是JSON Mode。你可以在系统提示词里要求模型"只输出JSON格式",也可以在API层开启JSON模式,甚至可以用vLLM、llama.cpp等推理框架支持的guided decoding,在解码阶段直接约束只能生成符合某个JSON Schema的Token序列,从根本上杜绝格式错误。

我实际用下来的经验是:光靠提示词说"输出JSON"不够稳,模型偶尔会输出Markdown代码块包着JSON,或者在里面夹注释。用guided decoding把这些都卡死,可靠性会好很多。如果所用的推理框架不支持约束解码,那就在输出层加一道防线:先用正则把Markdown代码块剥掉,再用容错JSON解析器,解析失败再重试一次并让模型修正。

这里有个容易被忽略的细节:结构化输出也会占用输出Token,而且约束多了会影响回答的自然度和信息量。所以不是所有场景都要JSON。

3.2 函数调用与工具调用:让模型不只是说话,而是发指令

函数调用(Function Calling)或者说工具调用,是我认为输出层最重要的创新。没有它,模型永远只是一个善于说话的人,有了它,模型才能成为一个能干活的人。

调用机制并不难理解。你在请求里声明一个工具清单,每个工具描述清楚名字、功能、参数结构。模型在回答时,生成的输出不是普通文本,而是一个类似这样的结构:

{ "tool_calls": [ { "function": "query_order_status", "arguments": { "order_id": "A12345" } } ] }

应用层解析这段输出,提取tool_calls字段,执行对应的函数,再把返回结果作为输入回填给模型,相当于跑完一次Agent循环。

这相当于你把可控的操作空间暴露给了模型,同时把可执行的动作边界卡在一个安全范围内。模型不能随便执行SQL或删除文件,它只能调用你定义好的几个函数。这种设计很符合实际工程的需要,安全性、可审计性都回来了。

我在实际项目里,会把"工具调用参数解析"当作一个专门的模块来处理。因为模型生成的参数偶尔会多一个逗号、少一个引号,或者把字段名拼成大小写不一致。不要指望模型生成的结构永远标准,输出层要做的是"防御式解析",失败了就让模型重新生成一次,这通常比写一堆容错正则更省心。

3.3 流式输出与中断控制

聊天应用要做流式输出(SSE),这个不用多说。后端把模型生成过程产生的Token增量通过SSE持续推给前端,前端一边接收一边渲染,用户的体验才从"干等几秒"变成"看着它打字"。

流式输出的实现要点有三个:

  • 后端不能等到模型全部生成完才返回,而是要拿到一个增量就直接推送给客户端。
  • SSE协议格式要处理好,每条消息以data:开头,末尾空行分隔,前端用EventSource或者fetch配ReadableStream来监听。
  • 要支持中断(abort)。如果用户在生成过程中点停止,前端要发一个中断信号,后端拿到信号后立即取消模型生成。这里要特别小心一个问题:中断之后缓存里的内容还没写完,不要把半截内容当作最终结果写入数据库。我见过有人把流式的中间态当成完整答案存了,后面解析JSON怎么都失败,排查了半天才发现是这个问题。

流式输出的好处不只是体验,它还让你有机会做"边生成边处理":比如模型输出JSON时,前端收到{"answer": "..."这个片段,就能先提取文本部分渲染出来,等到完整JSON到达后再解析引用字段。这种组合拳很适合做RAG问答类的页面。

3.4 输出侧的五类典型问题

输出侧常见的坑,我列一张表,都是我真实遇到过的:

问题表现常见解法
JSON截断输出到一半被max_tokens截断,解析失败调大输出上限;解析失败重试;用guided decoding约束
格式漂移时而JSON时而Markdown,时而中文引号后处理器兜底;输出schema约束
内容虚构回答看起来合理但引用了不存在的来源输出侧做引用校验,要求模型只引用输入材料
拒绝回答因为安全规则太强,合理问题也被拒调整系统提示词措辞;输入侧做脱敏后再请求
多次解析失败重试三次仍是坏JSON启动降温策略,比如降低temperature重新采样

你会发现,这些问题的很多根源其实在输入层和计算层,但修复的抓手几乎都在输出层。好的架构是:输出层永远保持"防御式"心态,默认模型会出错,解析代码要稳,失败就先重试而不是推翻整个流程。

4. 一个实战拆解:文档问答加Agent任务,三层分别怎么落地

4.1 场景一:文档问答机器人的三层设计

我最近在做的一个数据产品里,需要让用户直接问自然语言来查报表数据。这个场景如果不拆层,直接让大模型对着数据库表结构猜,结果一定很惨。拆成三层就好办了。

输入层做的事:用户问题进来后,先做一轮实体识别,把"上月华东区营收"提取成时间=上月、区域=华东、指标=营收,然后根据识别结果去查询元数据表,找到对应的数据表名和字段名,最后拼成一个带有清晰限定条件的查询描述,塞进上下文。

计算层:用一个中等规模的模型做语义理解,它不需要记住业务表的长相,只需要根据输入层已经加工好的材料和提示词生成对应的SQL语义描述,或者直接生成一段对数据的总结。

输出层:要求模型输出一个JSON,包含query_result_summary、sql、confidence三个字段。前端拿到结果后渲染成卡片,同时把sql送去人工审计。整个链路里,计算层几乎没花工夫,所有产品体验的差异都来自输入层的实体识别与上下文组织,以及输出层的结构化约束。

4.2 场景二:带工具调用的Agent循环

另一个场景是订单客服Agent,用户问"我上周买的蓝牙耳机怎么还没发货"。直接让模型回答,它没有订单数据。用三层框架设计后的流程是这样的:

第一步,输入层把用户ID和问题放进上下文,同时注入工具清单描述(包含query_order、create_refund_request这两个函数的定义)。

第二步,模型在输出层决策是否需要调工具。这里模型生成的不再是普通文本,而是一个tool_calls指令,指向query_order,参数是用户ID。

第三步,应用层执行工具,拿到订单状态JSON,然后作为新的输入层材料放回上下文。

第四步,模型看到工具返回结果后,最终在输出层生成一段对用户的解释。如果用户要求退款,模型还可以再走一轮create_refund_request调用。

这个循环用代码写出来大概是这个意思:

messages = [{"role": "system", "content": SYSTEM_PROMPT_WITH_TOOLS}, {"role": "user", "content": user_query}] while True: response = chat_model(messages, tools=TOOLS) if response.get("tool_calls"): tool_result = execute_tool(response["tool_calls"]) messages.append({"role": "assistant", "content": response["content"], "tool_calls": response["tool_calls"]}) messages.append({"role": "tool", "content": tool_result}) continue # 没有tool_calls,说明模型准备给最终答复 return response["content"]

这套写法的好处是,Agent的所有行为都是可审计的。每一轮输入输出都留在Messages里,出了问题可以直接追溯是模型选错工具、工具参数传错、还是工具返回数据有问题。

4.3 三层视角带来的排查方法论

项目跑起来之后,一定会出各种奇奇怪怪的问题。三层框架最大的实用价值就是给你一张排查地图。我的做法是,任何线上问题先按层分类定位:

现象优先查输入层优先查计算层优先查输出层
答非所问是,先看上下文是否污染、检索结果是否准确若输入没问题,看模型是否吃透了指令输出侧一般不会导致答非所问
回答内容错了但格式正常看材料是否过时、提示词是否有歧义看模型是否超出知识边界不用查
解析失败、格式错误看提示词对格式约束是否明确极少查JSON解析、guided decoding、截断
回答不动了、超时看是不是检索太慢看推理框架排队和显存看流式推送和abort逻辑

这个表格不是我凭空画的,是我被线上故障教育出来的。印象最深的一次,用户一直反馈回答特别空泛,我怎么看都以为是提示词不够清晰,反复改了好几天没有任何效果。后来用三层框架一查,才发现是上游数据源里某个字段权限没开,RAG检索回来的核心材料全是空记录。问题根本不在模型,而在输入层的材料供应链。

排查顺序我固定是:先查输入什么,再查输出怎么处理,最后才怀疑模型不行。因为输入和输出层的错误成本低、可修改性高,而模型层的问题往往只能通过换模型、调参或者微调来解决,代价大得多。

5. 容易被误解的边界:微调到底改的是哪一层

5.1 微调、部署、上下文:三个容易混淆的改动

说到这,肯定有人会问:那微调呢?微调不是改计算层吗?不是可以提升大模型的能力吗?这确实是个需要澄清的边界。

微调确实会修改计算层的参数。但我们要看清楚,微调的机制是在一个已经预训练好的模型基础上,用一定数量的"输入-输出对"继续做有监督训练。它改变的是模型在特定任务上对特定输入模式的响应路径,或者说,它改变的是"当输入长什么样时,模型更倾向于输出什么"。

从三层架构的视角看,微调介于两层之间:它修改的是计算层的权重,但它是通过大量构造输入-输出样本,从"输入和输出两端"去影响计算层的分布。这也是为什么很多团队误以为微调可以解决所有问题,实际上微调解决的核心问题通常是"让模型更听话、更适应某种输入习惯、更符合某种输出风格",但它并不能在计算层注入模型根本不知道的知识。

举个例子,你喂给模型十万条问答数据教它做历史考试题,它能学会答题套路,但当它遇到训练数据里没覆盖的新题目时,该不会还是不会。因为新知识要靠预训练才能进入参数矩阵,微调做不到无中生有。所以我的判断是:能不动计算层就不动,能把需求压到输入输出层解决就尽量在输入输出层解决。

5.2 "输入输出侧"不是提示词的代名词

还有一种常见的误读是:既然创新集中在输入输出两侧,那是不是只要把提示词写好,别的都不用管了?绝对不是。

输入侧包含了提示词,但不等于提示词。它还包括:

  • 检索系统(RAG的召回和精排)
  • 上下文管理(历史窗口、优先级、截断策略)
  • 多模态预处理(图像OCR、音频转写)
  • 数据权限和安全过滤(决定哪些材料能进上下文)

输出侧也不只是选一个解析器。它还包括:

  • 结构化模式控制
  • 工具调用协议
  • 流式传输和中断
  • 后处理校验(引用校验、敏感词检查、幻觉检测)

如果你把"输入什么/输出怎么处理"简单理解成"把prompt写长一点、把response解析一下",那就等于把三层架构这个工具的威力浪费了九成。

5.3 边界认知带来的实际决策价值

理解了边界,做技术决策时思路会清晰不少。比如:

  • 如果是用户反馈"回答总是没有业务深度",先别急着上大模型微调,先检查输入侧有没有把业务材料、术语表、内部文档递给模型。数据都没喂进去,指望模型靠参数里的常识回答业务细节,不现实。
  • 如果是用户反馈"回答总是不符合格式",不要去调温度,也不要去微调,先优化输出侧的schema约束和解析兜底。
  • 如果是用户反馈"同一个问题有时候对有时候错",那大概率是输出侧采样随机性带来的波动,可以考虑固定seed、降低temperature,或者输出侧加一次自校验。
  • 如果是模型对某个专有领域的理解确实不行,且你手里有大量高质量的问答数据,再考虑用微调把领域范式固化下来。

这套判断逻辑几乎适用于所有大模型落地方案。它让你不再被"大模型很聪明"这个幻觉牵着走,而是回到工程视角去定位问题和优化。

6. 最后再分享三个我自己常用的调试技巧

6.1 技巧一:用可控随机性区分"知识问题"和"抽样问题"

遇到模型输出飘忽不定的时候,先把temperature调成0,把seed固定住,然后连续跑三次。如果三次输出基本一致但还是错的,那是知识或输入材料的问题,去查输入层;如果三次输出完全不一样,那是采样随机性造成的,去调输出层的解码参数或者加约束,比换模型更有效。

6.2 技巧二:把完整输入打印出来看一遍

绝大多数我看到的问题,只要把真正送给模型的完整Messages打印出来,瞬间就能找到答案。比如发现系统提示词后面跟了一大段检索出来的无关博客,或者发现用户的历史对话里夹着上一轮的工具报错信息,这种问题光靠改提示词是永远修不好的。我会在开发环境里加一个开关,记录每一轮请求的完整输入Token内容,排查问题时先看输入,再看输出。这个习惯帮我省了一半以上的排查时间。

6.3 技巧三:输出侧永远留一条"兜底路径"

不管前端还是后端,都不要默认模型输出的结构是完美的。我的做法是,每个输出处理模块都至少留两条路径:一条正规路径,按预期结构解析;一条兜底路径,解析失败时把原始文本原样展示给用户或者返回重新生成,绝不让页面因为一个JSON解析异常直接崩掉。产品上线后,兜底路径的使用率就是模型稳定性的一个量化指标,比靠感觉判断靠谱得多。

这三层架构的思维方式,我用了很长时间才真正内化。它不解决某一个具体Bug,但它能让所有AI相关的问题都有了一个归属地。我遇到任何新玩法的第一反应,永远是先去拆它的输入层和输出层分别做了什么,拆完了,这个项目到底是什么水平,心里基本就有数了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进