1. 为什么“三层架构”是理解大模型应用的万能钥匙
很多人第一次接触大模型应用开发时,会被各种新名词砸晕:提示工程、思维链、RAG、Agent、Function Calling、结构化输出、流式响应……每隔几周就冒出一个新概念,感觉永远追不完。但如果你把这些花样全部摊开来看,会发现一个让人安心的事实:几乎所有大模型应用,本质上都在做三件事——决定输入什么、让模型怎么想、处理模型输出什么。
这就是我所说的“三层架构”。它不是某个框架的官方定义,而是我在做过十几个大模型落地项目之后,自己总结出来的一套拆解视角。一旦你脑子里有了这三层,再看任何新出的AI工具或论文,都能在五分钟内把它归类:它到底是在改输入层、推理层,还是输出层?改的是哪一层的哪个环节?它真正的创新点在哪里,还是只是换了个包装?
先给一个最简化的三层模型,后面所有内容都围绕它展开:
| 层级 | 核心问题 | 典型手段 |
|---|---|---|
| 输入层 | 给模型看什么 | 提示词设计、上下文拼接、示例选择、检索增强 |
| 推理层 | 模型怎么处理 | 思维链、分步推理、多轮自我检查、工具调用 |
| 输出层 | 拿到的结果怎么用 | 结构化解析、格式校验、后处理、流式渲染 |
你可能会问:那模型本身的训练、微调、参数规模算什么?那些属于“模型能力”这个底座,是另一条线。我们这里讨论的是应用层的架构——也就是说,当你手里已经有一个能调用的大模型API时,你怎么围绕它搭建一个真正能用的系统。这个视角对绝大多数开发者和产品经理来说,才是最实用的。
我见过太多团队一上来就说“我们要微调模型”,结果聊了半小时才发现,他们的问题其实在输入层——提示词写得含糊不清,模型根本不知道要干什么。也见过有人花大力气做Agent框架,最后发现输出层没有做校验,模型返回的JSON格式稍微一变,整个系统就崩了。三层里任何一层没做好,另外两层做得再漂亮也白搭。
接下来的内容,我会把这三层逐层拆开,讲清楚每层的核心逻辑、常见做法、容易踩的坑,以及我在实际项目中总结出来的经验。每一层我都会给出可以直接参考的代码示例和配置思路。读完之后,你应该能对着任何一个大模型应用需求,快速判断出它主要涉及哪几层,每层该用什么方案。
2. 输入层:决定模型上限的不是模型本身,而是你喂了什么
2.1 提示词不是“随便写一段话”,而是接口设计
很多人对提示词的理解还停留在“写一段话让模型干活”。这个理解在玩一玩的时候没问题,但一旦要上生产,提示词就必须被当作接口契约来对待。什么叫接口契约?就是你要明确规定:输入是什么格式、输出是什么格式、边界条件怎么处理、异常情况怎么兜底。
我举个实际例子。假设你要做一个“从用户评论中提取情感倾向和关键问题”的功能。随手写的提示词可能是这样的:
请分析以下评论的情感倾向和关键问题: {comment}这个提示词能用吗?能跑通demo,但上不了生产。因为模型每次返回的格式可能都不一样——有时候是“情感:正面;问题:物流太慢”,有时候是“这条评论表达了正面情感,主要问题是物流速度”,有时候还会加一句“希望对您有帮助”。你的下游程序根本没法稳定解析。
正确的做法是把提示词当成一个函数签名来设计:
PROMPT_TEMPLATE = """你是一个评论分析引擎。请严格按照以下JSON格式输出分析结果,不要添加任何额外文字。 输入评论: {comment} 输出格式: {{ "sentiment": "positive | negative | neutral", "confidence": 0.0 到 1.0 之间的浮点数, "issues": ["问题1", "问题2"], "summary": "一句话总结" }} 要求: 1. sentiment 只能是 positive、negative、neutral 三者之一 2. confidence 表示你对情感判断的置信度 3. issues 列出评论中提到的具体问题,没有则返回空数组 4. summary 不超过30个字 """你看,这就从“写一段话”变成了“定义一个接口”。输入有明确的占位符,输出有明确的schema,边界条件也写清楚了。提示词设计的本质,是在自然语言的灵活性和程序接口的确定性之间找平衡。
提示:不要指望模型“理解你的意图”。模型只会按照你字面写的东西去执行。你觉得“它应该知道”的事情,它大概率不知道。把所有隐含假设都显式写出来,是提示词设计的第一原则。
2.2 上下文拼接:把什么放进窗口,比放多少更重要
大模型的上下文窗口越来越大,从最早的4K到现在的128K甚至更大。但窗口大不代表你就应该把所有东西都塞进去。我见过一个团队做客服机器人,把整个产品文档、历史对话、用户信息全部拼在一起,每次请求塞进去几万字。结果呢?响应慢、成本高,而且模型经常“迷失”在中间——前面说的规则它记得,中间的关键信息它忽略了,最后又跑偏了。
这里有一个被反复验证的现象:模型对上下文开头和结尾的信息最敏感,中间部分容易被忽略。所以上下文拼接的核心不是“放多少”,而是“放什么、放在哪”。
我的做法是给上下文分优先级,按以下顺序拼接:
- 系统指令(放在最前面):定义角色、输出格式、核心约束
- 当前任务相关的关键信息(放在系统指令之后):比如用户当前问题、检索到的相关文档片段
- 少量示例(放在中间偏后):如果任务复杂,给1-3个输入输出示例
- 当前输入(放在最后):用户这次实际要处理的内容
为什么这样排?因为系统指令是“宪法”,必须让模型最先看到;当前输入是“考题”,放在最后能让模型在生成时最近距离地参考;示例放在中间,起到“参考格式”的作用,但不会喧宾夺主。
def build_context(system_prompt, examples, retrieved_docs, user_input): parts = [] parts.append(f"【系统指令】\n{system_prompt}") if retrieved_docs: docs_text = "\n---\n".join(retrieved_docs) parts.append(f"【参考资料】\n{docs_text}") if examples: examples_text = "\n".join( f"输入:{e['input']}\n输出:{e['output']}" for e in examples ) parts.append(f"【示例】\n{examples_text}") parts.append(f"【当前输入】\n{user_input}") return "\n\n".join(parts)这个拼接顺序不是拍脑袋定的,而是基于一个很实际的考虑:模型生成输出时,对最近看到的内容注意力权重最高。把当前输入放在最后,就是利用这个特性,让模型在生成时“盯着”当前任务。
2.3 检索增强:不是所有问题都需要RAG,但需要RAG的问题必须做好
RAG(检索增强生成)这两年火得一塌糊涂,好像不做个向量数据库都不好意思说自己在做大模型应用。但我要泼一盆冷水:大部分场景其实不需要RAG。如果你的知识是固定的、量不大的,直接写在提示词里就行。RAG解决的是“知识太多、变化太快、无法全部塞进提示词”的问题。
那什么时候该上RAG?我总结了一个简单的判断标准:
| 场景特征 | 是否需要RAG |
|---|---|
| 知识总量小于模型窗口的1/3 | 不需要,直接拼接 |
| 知识频繁更新,每天都有新内容 | 需要 |
| 知识有明确的结构化来源(数据库、文档库) | 需要 |
| 需要精确引用来源 | 需要 |
| 知识是通用常识 | 不需要,模型自己知道 |
如果你确定要上RAG,那检索质量就是生死线。我见过太多RAG系统,检索出来的内容跟问题八竿子打不着,模型只能硬着头皮编。检索做不好,后面的生成再强也没用。
检索环节有几个关键决策点:
切块策略:文档怎么切?按固定字数切是最省事的,但效果往往最差。更好的做法是按语义切——一个段落、一个小节、一个完整的论述单元。我通常会用“标题+正文”的方式切块,每个块保留所属的章节标题,这样检索出来的时候能保留上下文。
向量模型选择:不是越贵越好。中文场景下,有些开源模型在特定领域比通用大模型的效果更好。关键是看你的文档类型——技术文档、法律文书、客服对话,适合的向量模型不一样。我的建议是先用一个中等规模的模型跑起来,用真实问题测召回率,不行再换。
混合检索:纯向量检索有个致命问题——对精确匹配不敏感。比如用户问“错误码E5023是什么意思”,向量检索可能返回一堆“错误处理”相关的文档,但就是找不到E5023这个具体条目。这时候需要结合关键词检索(BM25),把精确匹配的结果也捞回来,然后做融合排序。
def hybrid_retrieve(query, vector_store, bm25_index, top_k=5): # 向量检索 vector_results = vector_store.search(query, top_k=top_k*2) # 关键词检索 keyword_results = bm25_index.search(query, top_k=top_k*2) # 融合排序:RRF(倒数排名融合) scores = {} for rank, doc in enumerate(vector_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) for rank, doc in enumerate(keyword_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [doc_id for doc_id, _ in sorted_docs[:top_k]]RRF里的60是个经验常数,来自原论文的推荐值,实际用的时候可以根据效果微调。这个融合方法的好处是不需要调权重,两种检索方式各出一份排名,按排名倒数求和,简单有效。
2.4 少样本示例:给例子比讲道理管用
如果你想让模型按照特定风格或格式输出,最有效的方法不是写一堆规则,而是给几个例子。这在提示工程里叫“少样本提示”(Few-shot Prompting)。模型看到例子之后,会自然地模仿例子的模式,比你用文字描述规则要准确得多。
但给例子也有讲究。我踩过的坑包括:
- 例子太多:给了十几个例子,把上下文占满了,模型反而抓不住重点。通常3-5个就够了,覆盖主要的变化情况即可。
- 例子太相似:所有例子都是同一种情况,模型遇到稍微不同的输入就懵了。例子要覆盖边界情况——正常的、异常的、简短的、复杂的,各来一个。
- 例子顺序固定:模型对最后一个例子印象最深。如果你希望模型偏向某种输出风格,把最符合期望的那个例子放在最后。
还有一个进阶技巧:动态选择示例。不是每次都用同一组例子,而是根据当前输入,从示例库里检索最相似的几个例子拼进去。这样示例的针对性更强,效果通常比固定示例好一截。
3. 推理层:让模型“想清楚”再回答的几种手段
3.1 思维链:为什么“一步一步来”真的有用
思维链(Chain-of-Thought)可能是大模型领域最反直觉的发现之一:仅仅在提示词里加一句“让我们一步一步思考”,模型在数学题、逻辑题上的准确率就能大幅提升。这听起来像玄学,但背后的原理其实不复杂。
大模型生成文本是逐token进行的,每个token的生成只依赖于前面已经生成的token。如果模型直接输出答案,那它必须在看到问题的瞬间就“算”出结果,没有中间草稿。但如果模型先输出推理步骤,那这些步骤就变成了后续生成的“上下文”,模型可以基于自己写出的中间结果继续推导。换句话说,思维链给了模型一块“草稿纸”。
这个机制在实际应用中的价值在于:对于任何需要多步推理的任务,都应该让模型先输出推理过程,再输出最终答案。但这里有个坑——如果你直接把推理过程也返回给用户,用户体验会很差。用户只想要答案,不想看模型“自言自语”。
所以正确的做法是:让模型输出推理过程,但在返回给用户之前把推理部分截掉,只保留最终答案。或者用结构化输出,把推理和答案放在不同字段里。
COT_PROMPT = """请先分析问题,然后给出答案。严格按照以下格式输出: 【分析】 (在这里写出你的推理过程) 【答案】 (在这里写出最终答案) 问题:{question} """解析的时候,用正则把【答案】后面的内容提取出来就行。这样既利用了思维链提升准确率,又保证了用户看到的输出是干净的。
3.2 自洽性投票:同一个问题问多遍,取多数结果
思维链虽然有用,但单次推理仍然可能出错。一个简单粗暴但有效的改进方法是:让模型用不同的推理路径多次回答同一个问题,然后取多数结果。这叫自洽性(Self-Consistency)。
具体操作是:把温度参数调高一点(比如0.7),让模型每次生成不同的推理过程,跑5次,得到5个答案,然后投票。对于有确定答案的任务(比如数学题、分类任务),多数投票能显著提升准确率。
这个方法的代价是成本翻倍——跑5次就是5倍的token消耗。所以它适合那些“答错代价很高、但答对价值也高”的场景,比如医疗问答、法律咨询、财务计算。对于普通场景,单次思维链就够了。
我实际用下来,自洽性投票在分类任务上的提升最明显。比如情感分析,单次准确率可能85%,跑5次投票能到92%左右。但到了95%以上之后,再增加投票次数收益就很小了。
3.3 工具调用:让模型知道自己“不知道”
大模型有个毛病:它不知道自己不知道。你问它今天天气怎么样,它可能编一个出来。你问它某个实时数据,它可能给你一个过时的数字。这不是模型故意骗你,而是它的训练目标就是“生成看起来合理的文本”,而不是“只说自己确定的事情”。
工具调用(Function Calling / Tool Use)就是解决这个问题的。它的逻辑是:给模型一组工具定义,让模型自己判断什么时候需要调用工具。如果模型觉得自己的知识不够,它就会输出一个工具调用请求,而不是硬编一个答案。
工具调用的实现通常分两步:
第一步,定义工具。每个工具需要描述清楚:叫什么名字、干什么用的、需要什么参数、参数是什么类型。
{ "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如北京、上海" }, "date": { "type": "string", "description": "日期,格式YYYY-MM-DD,不填则查今天" } }, "required": ["city"] } }第二步,处理模型的工具调用请求。模型不会真的去调你的API,它只会输出一个结构化的请求,比如{"tool": "get_weather", "args": {"city": "北京"}}。你的程序拿到这个请求后,去实际调用天气API,把结果再喂回给模型,让模型基于真实数据生成最终回答。
这个流程的关键在于:工具描述要写得让模型能准确判断什么时候该用。我见过有人把工具描述写得含糊不清,结果模型该调用的时候不调用,不该调用的时候乱调用。工具描述要像给新员工写操作手册一样,把使用场景、参数含义、返回格式都写清楚。
3.4 多轮自我检查:让模型当自己的审稿人
还有一个很实用的技巧:让模型生成答案后,再让它自己检查一遍。具体做法是在提示词里加一段:
请检查你上面的回答: 1. 是否有事实错误? 2. 是否遗漏了重要信息? 3. 格式是否符合要求? 如果有问题,请修正后重新输出。这个“自我检查”的步骤能让模型发现自己的一些低级错误。原理还是那个——模型在生成检查意见时,相当于换了一个视角重新审视前面的内容,一些在生成时没注意到的问题,在检查时会被发现。
但要注意:自我检查不是万能的。如果模型第一次就理解错了问题,自我检查也救不回来。它主要能修正的是格式错误、遗漏、明显的逻辑矛盾。对于深层的理解偏差,还是得靠更好的输入层设计。
4. 输出层:模型返回的文本不是终点,而是起点
4.1 结构化输出:让模型返回程序能直接用的数据
模型返回的是自然语言文本,但你的程序需要的是结构化数据。这中间的转换,就是输出层要解决的核心问题。
最直接的方法是让模型直接输出JSON。现在很多模型API都支持“JSON模式”,你可以在请求参数里指定输出格式为JSON,模型会保证输出是合法的JSON。但即使没有这个功能,通过提示词约束也能达到类似效果。
不过,不要盲目信任模型输出的JSON。我踩过的坑包括:模型在JSON外面包了一层json代码块标记、模型在JSON后面加了一句“希望对您有帮助”、模型把数字写成了字符串、模型漏掉了某个字段。所以拿到模型输出后,必须做校验和清洗。
import json import re def parse_model_json(raw_output): # 去掉可能的代码块标记 cleaned = re.sub(r'^```json\s*', '', raw_output.strip()) cleaned = re.sub(r'\s*```$', '', cleaned) # 尝试找到第一个 { 和最后一个 } start = cleaned.find('{') end = cleaned.rfind('}') if start == -1 or end == -1: raise ValueError("输出中未找到JSON对象") json_str = cleaned[start:end+1] try: data = json.loads(json_str) except json.JSONDecodeError as e: raise ValueError(f"JSON解析失败:{e}") return data如果对格式要求特别严格,可以用Pydantic这样的库做schema校验。定义好每个字段的类型和约束,解析后自动验证,不通过就触发重试或降级处理。
4.2 流式输出:用户体验的关键优化
大模型生成完整回答可能需要几秒到几十秒。如果等全部生成完再一次性返回,用户会盯着空白屏幕等很久,体验很差。流式输出(Streaming)解决的就是这个问题——模型每生成一个token就立刻推送给前端,用户能看到文字一个一个蹦出来。
流式输出的技术实现通常用SSE(Server-Sent Events)或WebSocket。后端调用模型API时开启stream模式,拿到一个生成器,然后逐块转发给前端。
async def stream_response(prompt): async for chunk in model_client.generate_stream(prompt): # chunk 可能是单个token,也可能是多个token yield f"data: {json.dumps({'text': chunk})}\n\n" yield "data: [DONE]\n\n"前端用EventSource接收,每收到一个数据块就追加到页面上。这个体验上的提升是巨大的——用户感觉系统“在思考”,而不是“卡死了”。
但流式输出也有代价:你没法在输出过程中做完整的格式校验。因为数据是一块一块来的,你没法等全部收完再校验。所以流式输出通常用在那些对格式要求不高的场景,比如聊天、问答。如果下游需要严格的结构化数据,还是得等完整输出后再解析。
4.3 后处理与兜底:模型一定会出错,关键是出错之后怎么办
我在项目里有一个铁律:永远假设模型会出错,并且提前想好出错之后怎么办。模型可能返回空、可能返回格式错误的内容、可能返回跟问题无关的东西、可能触发内容安全策略被拦截。这些情况都必须有兜底方案。
常见的兜底策略包括:
- 重试:格式解析失败时,把错误信息拼回提示词,让模型重新生成。通常重试1-2次就能解决大部分格式问题。
- 降级:如果重试仍然失败,返回一个默认值或友好的错误提示,而不是把异常抛给用户。
- 人工兜底:对于关键业务,把失败案例记录下来,进入人工审核队列。
def robust_generate(prompt, max_retries=2): for attempt in range(max_retries + 1): try: raw = model_client.generate(prompt) data = parse_model_json(raw) validate_schema(data) return data except Exception as e: if attempt == max_retries: log_error(f"生成失败,已重试{max_retries}次:{e}") return get_fallback_response() # 把错误信息拼回提示词,让模型修正 prompt = f"{prompt}\n\n上次输出有误:{e}\n请修正后重新输出。"这个重试逻辑看起来简单,但在实际项目里能挡掉80%以上的偶发格式问题。关键是要把错误信息反馈给模型,而不是简单地重试同样的提示词。模型看到具体的错误原因后,修正的成功率会高很多。
4.4 输出格式的“渐进式约束”
还有一个我在实践中总结出来的技巧:不要一次性要求模型输出非常复杂的结构。如果你要模型输出一个嵌套很深的JSON,字段很多,模型很容易出错。更好的做法是分步输出,每一步只要求一个简单的结构,然后在程序里组装。
比如你要生成一份报告,包含摘要、正文、结论三部分。与其让模型一次性输出完整的JSON,不如分三次调用:第一次生成摘要,第二次生成正文,第三次生成结论。每次的输出都很简单,模型不容易出错,而且你可以对每一部分单独做校验和重试。
这个思路的本质是:把复杂的输出任务拆解成多个简单的输出任务,用程序的确定性来弥补模型的不确定性。模型擅长生成内容,但不擅长维护复杂的结构。让模型做它擅长的事,结构的事情交给代码。
5. 三层视角下的常见误区与排查思路
5.1 效果不好时,先定位是哪一层的问题
很多人遇到模型效果不好,第一反应是“模型不行,换个更大的”。但根据我的经验,大部分效果问题都不在模型本身,而在输入层或输出层。下面这个排查清单,是我在实际项目中反复使用的:
| 症状 | 可能的问题层 | 排查方向 |
|---|---|---|
| 回答跑题 | 输入层 | 提示词是否清晰?任务描述是否明确? |
| 格式不对 | 输出层 | 是否给了格式示例?是否有校验和重试? |
| 事实错误 | 输入层/推理层 | 是否需要RAG?是否该用工具调用? |
| 推理错误 | 推理层 | 是否用了思维链?是否需要分步? |
| 回答太长/太短 | 输入层 | 是否指定了字数或详细程度? |
| 风格不对 | 输入层 | 是否给了风格示例? |
| 偶尔出错 | 输出层 | 是否有重试和兜底? |
这个表我建议你打印出来贴在工位上。遇到问题先查表,能省下大量瞎试的时间。
5.2 不要过早引入复杂框架
现在市面上有很多大模型应用框架,功能很全,但我的建议是:先用最朴素的方式跑通,再考虑要不要上框架。很多框架封装了太多东西,出了问题你根本不知道是哪一层的问题。用原生API调用,每一层的输入输出你都看得清清楚楚,调试起来快得多。
我自己的做法是:先用Python脚本直接调API,把输入层、推理层、输出层都手动实现一遍。跑通之后,如果确实需要更复杂的功能(比如多Agent协作、复杂的工具编排),再考虑引入框架。这时候你对每一层的理解已经很深了,用框架也能用得明白。
5.3 成本控制的三个杠杆
大模型应用的成本主要来自token消耗。控制成本有三个杠杆,分别对应三层:
输入层:精简提示词,去掉不必要的示例和上下文。能用短提示词解决的,不要用长提示词。RAG检索时控制返回的文档数量,不要一股脑塞进去。
推理层:不是所有任务都需要思维链。简单的分类、提取任务,直接输出答案就行,不需要“一步一步思考”。工具调用也要控制,不要让模型动不动就调工具。
输出层:控制输出长度。在提示词里明确要求“简洁回答”或“不超过X字”。流式输出虽然体验好,但如果下游不需要实时展示,可以关掉流式,减少连接开销。
我做过一个粗略的统计:在一个典型的问答场景里,输入层的token消耗通常占60%以上。所以优化输入层是降本的第一优先级。把提示词从500字精简到200字,成本直接降一半,效果可能还更好——因为模型不用在冗余信息里找重点了。
6. 把三层架构用起来:一个完整的实战拆解
6.1 需求:从用户反馈中自动提取结构化信息
假设你有一个产品,每天收到几百条用户反馈。你需要自动从每条反馈中提取:问题类型、严重程度、涉及的功能模块、用户情绪。输出要直接入库,所以格式必须严格。
这个需求看起来简单,但如果不按三层架构来设计,很容易做成一个“demo能用、上线就崩”的系统。
6.2 输入层设计
首先明确任务:从一段自由文本中提取四个字段。输入层的核心是提示词设计。
SYSTEM_PROMPT = """你是一个用户反馈分析引擎。你的任务是从用户反馈中提取结构化信息。 输出必须严格遵循以下JSON格式,不要添加任何额外文字: { "category": "bug | feature_request | complaint | praise | other", "severity": "critical | high | medium | low", "module": "登录 | 支付 | 搜索 | 个人中心 | 其他", "sentiment": "positive | negative | neutral", "summary": "不超过20字的问题概括" } 分类标准: - bug:功能异常、报错、崩溃 - feature_request:希望增加新功能 - complaint:对现有功能不满但非bug - praise:表扬或正面反馈 - other:无法归入以上类别 严重程度标准: - critical:影响核心功能使用,如无法登录、无法支付 - high:影响主要功能,但有替代方案 - medium:影响次要功能或体验 - low:轻微问题或建议 """这个提示词的关键点:每个字段都给了明确的枚举值和判断标准。模型不需要“猜”你想要什么,它只需要按图索骥。
6.3 推理层设计
这个任务其实不需要复杂的推理。分类和提取属于“感知型”任务,模型直接输出就行。加思维链反而可能引入噪声——模型在“分析”过程中可能想太多,把简单问题复杂化。
但有一个地方需要推理:严重程度的判断。用户说“登录不了”,这算critical还是high?如果用户是普通用户,登录不了就是critical;如果用户是管理员,可能还有后台通道。这个判断需要结合业务规则。
我的做法是在提示词里把规则写死,不让模型自由发挥。如果规则太复杂,提示词写不下,那就用工具调用——让模型先判断问题类型,然后程序根据类型查表决定严重程度。这样把推理的确定性部分交给代码,不确定性部分交给模型。
6.4 输出层设计
输出层的核心是校验和兜底。模型返回JSON后,必须做以下检查:
- JSON是否合法
- 所有必需字段是否存在
- 枚举字段的值是否在允许范围内
- summary字段是否超过长度限制
VALID_CATEGORIES = {"bug", "feature_request", "complaint", "praise", "other"} VALID_SEVERITIES = {"critical", "high", "medium", "low"} VALID_MODULES = {"登录", "支付", "搜索", "个人中心", "其他"} VALID_SENTIMENTS = {"positive", "negative", "neutral"} def validate_feedback(data): errors = [] if data.get("category") not in VALID_CATEGORIES: errors.append(f"category无效:{data.get('category')}") if data.get("severity") not in VALID_SEVERITIES: errors.append(f"severity无效:{data.get('severity')}") if data.get("module") not in VALID_MODULES: errors.append(f"module无效:{data.get('module')}") if data.get("sentiment") not in VALID_SENTIMENTS: errors.append(f"sentiment无效:{data.get('sentiment')}") if len(data.get("summary", "")) > 20: errors.append("summary超过20字") return errors如果校验不通过,就把错误信息拼回提示词重试。重试两次还不行,就降级处理——把原始反馈和错误信息一起存入“待人工处理”队列,不阻塞主流程。
6.5 这个拆解带来的实际收益
按这个三层架构实现之后,这个反馈分析系统的准确率从最初的70%左右提升到了93%以上。提升主要来自三个方面:
- 输入层的枚举值约束,让模型的输出空间从“无限可能”缩小到了“几个选项”
- 输出层的校验和重试,挡掉了大部分偶发格式错误
- 降级机制保证了即使模型出错,系统也不会崩溃
更重要的是,整个系统的行为变得可预测了。你知道模型在什么情况下会输出什么,知道出错时会发生什么,知道怎么调试。这种确定性,是上生产环境的基本要求。
7. 三层架构的边界与我的个人体会
三层架构不是万能的。它主要适用于应用层的开发和调试,也就是你调用现成模型API来构建系统的场景。如果你要做模型训练、微调、蒸馏,那是另一个维度的事情,三层架构帮不上太多忙。
另外,三层之间的界限也不是绝对的。比如RAG,它既涉及输入层(检索什么放进去),也涉及推理层(模型怎么利用检索结果)。工具调用也是,定义工具属于输入层,决定调用属于推理层,处理返回结果属于输出层。所以三层是一个分析框架,不是严格的模块划分。它的价值在于帮你快速定位问题、组织思路,而不是给你一个死板的分类标准。
我自己在实际项目里最大的体会是:大模型应用的质量,80%取决于输入层和输出层的设计,只有20%取决于模型本身。同一个模型,提示词写得好不好,输出校验做得严不严,效果可以差出好几倍。所以与其花时间研究哪个模型更强,不如把精力花在把三层架构的每一层做扎实。
还有一个很实际的建议:每做一个新功能,先画一张三层图。左边写输入层要放什么,中间写推理层要做什么处理,右边写输出层要怎么校验和兜底。这张图花五分钟就能画完,但能帮你避免大量的返工。我现在的习惯是,任何大模型相关的需求,不画三层图不动手写代码。
最后说一个我踩过的坑。早期做项目时,我总想把所有东西都塞进一个提示词里,觉得“模型这么强,应该能处理”。结果就是提示词越写越长,效果越来越差,调试越来越难。后来学会拆解之后,把一个大任务拆成多个小任务,每个任务单独设计输入输出,用程序串联起来。虽然调用次数多了,但每次调用都很简单、很稳定,整体效果反而更好,调试也容易得多。模型不是越用越强,而是越用越“窄”越好用。让每次调用只做一件简单的事,把复杂的逻辑交给代码,这是我做了这么多项目之后最深的体会。