1. 先聊聊为什么“简单至上”在大模型开发里这么难1.1 单点复杂度低组合复杂度高接触过不少从传统软件切到大模型应用开发的团队最典型的状态是第一周特别兴奋第二周开始迷茫第三周开始堆框架。原因其实很朴素——单独看大模型调一次接口确实简单得不像话。你给它一段文本它回你一段文本和调用一个普通HTTP服务没有本质区别。但一旦进入真实业务场景事情就开始悄悄变味。今天的对话要带历史上下文明天要接企业内部文档后天要支持多轮追问再往后要做权限隔离、内容审核、成本统计。每加一个需求链路里就多一个环节。等回过神来看架构图上面已经画满了检索器、路由层、记忆模块、向量数据库、回调机制。更麻烦的是这些模块之间的交互大多是非确定性的模型一升级可能整条链路的行为都变了。我的一个参考项目是某内部客服系统。最初的版本只有一个小函数调用远端模型接口把用户问题传过去返回答案显示给客服。代码不到两百行线上跑了一个月效果稳定。后来业务方提出要支持“根据历史工单回答同类问题”团队很快引入了向量检索又加上重排模型再后来为了减少Token消耗加上了缓存层和上下文压缩模块。模块多了以后问题排查开始变得非常痛苦用户说“答案不对”你根本分不清是检索没召回还是重排打错了还是提示词里历史信息太多导致模型被带偏。从工程视角看这就是“单点复杂度低、组合复杂度高”的典型表现。单看每个模块都是合理的但把它们串在一起以后系统的故障边界、延迟特征、成本构成都会发生质变。简单的直觉在这里会失灵。1.2 “炫技式”复杂度是怎么悄悄进来的我观察到一个很普遍的情况大模型应用开发里很多复杂度不是业务逼出来的而是开发过程中“顺手”加进去的。比如明明可以直接把命中的文档拼进提示词里却非要先接一套编排框架用它的链式调用机制明明一个原生API调用就能解决却为了“以后好扩展”先抽象出三层接口明明业务只要求单轮问答却把流式输出、多模态解析、插件调用全设计进去。技术选型的时候大家都会说“为了长期演进”但事实上大部分长链路在第一个月根本用不上。更深层的问题在于大模型应用的输出质量本身带有随机性一次良好的运行结果很难说明“系统是好的”。这种不确定性会诱发一种心态与其相信一个简单的提示词模板不如把能加的控制手段全加上仿佛组件越多就越可控。可是大多数情况下组件越多变量越多结果反而越难稳定。模型行为出问题的时候你要同时检查的东西也成倍增加。有一次排查一个问答失常的问题前后三天。最后定位出来是链路中一个“文档切分器”在某种标点组合下把段落切成了一段几乎为空的内容导致模型只能靠提示词里的通用指令猜答案。整个链路里有四五个组件都有嫌疑从缓存到路由到检索再到模板渲染。如果当时只有“取文档、拼提示词、调模型”三步问题十分钟就该现形。1.3 简单不是砍功能而是控制必要复杂度必须说清楚我这里说的“简单至上”不是让大家把系统做成一个裸的API壳子也不是排斥RAG、多智能体这类技术。它的意思是每一份复杂度都必须由明确、可验证的业务收益来支撑。如果一个模块无法证明它带来了指标提升、成本下降或体验改善它就不该出现在主干链路上。我在项目里通常做一个“复杂度登记表”每引入一个组件必须填写引入原因、预期收益、验证方法、回滚成本。这样做有两个好处一是强迫自己把“跟风”和“需要”区分开二是一旦效果不如预期可以快速下线。《简单至上》不是说“绝不引入复杂工具”而是“复杂必须通过验收才能留下来”。这套思路实际执行起来最难的其实是拒绝。业务方提需求容易架构师守住边界难。我的经验是先按最简单的方式做一版把业务跑通让真实数据说话再决定要不要上复杂度。大多数时候你会发现提示词调一调、示例加几个问题就解决了根本不需要新增组件。2. 稳定高效的前提先把调用链路做扎实很多大模型应用翻车不是死在模型智商上而是死在工程基本功上。网络超时没处理用户等了几秒直接放弃重试策略写得不对同一笔订单被重复扣费日志里没有记录Token消耗月底对账才发现成本翻了两倍。这些问题跟模型能力毫无关系纯粹是调用链路没打牢。2.1 基础调用优先原厂SDK框架能不上就不上选型上我的原则很简单第一优先是模型服务商官方提供的SDK第二优先是封装很薄的语言原生HTTP客户端第三才考虑通用编排框架。很多人一上来就上重型框架理由是“以后可能要多厂商切换”。但多厂商切换本身是个伪需求绝大多数业务在设计时要的根本不是“切换能力”而是“万一服务挂了能用别的顶上”。这只需要在调用层做一个统一的接口抽象就够了几十行代码的事不需要因此引入一整层框架依赖。以我常用的Python技术栈为例一个最朴素但完整的调用层长这样import time from typing import Optional import requests class LLMClient: 极简的模型调用客户端只做三件事调用、重试、超时控制。 def __init__(self, api_key: str, base_url: str, model: str): self.api_key api_key self.base_url base_url self.model model self.session requests.Session() self.session.headers.update({ Authorization: fBearer {api_key}, Content-Type: application/json, }) def complete( self, messages: list[dict], temperature: float 0.2, max_tokens: int 1024, timeout: int 30, max_retries: int 3, ) - tuple[str, dict]: payload { model: self.model, messages: messages, temperature: temperature, max_tokens: max_tokens, } last_exc: Optional[Exception] None for attempt in range(max_retries): try: resp self.session.post( f{self.base_url}/chat/completions, jsonpayload, timeouttimeout, ) resp.raise_for_status() data resp.json() content data[choices][0][message][content] usage data.get(usage, {}) return content, usage except (requests.ConnectionError, requests.Timeout) as exc: last_exc exc wait 2 ** attempt time.sleep(wait) raise RuntimeError(fLLM call failed after {max_retries} retries: {last_exc})这个类没有任何花哨功能但包含了大模型调用最核心的三个工程要件超时控制、指数退避重试、结果和用量信息返回。别小看这三件事真实线上出问题八成以上都跟它们有关。没有超时时间一个卡住的请求可能拖垮整个线程池没有重试机制一次网络抖动就直接让用户看到报错不记录Token用量你根本不知道自己烧了多少钱。2.2 必须写进代码的超时、重试和熔断超时参数不是随便填的。大模型接口的响应时间和输入长度强相关文档检索越多、上下文越长首字延迟就越大。我通常把业务可接受的最长等待时间作为底线再预留一部分余量。比如产品要求最差3秒内出结果那接口超时我会设成8到10秒给网络抖动留出余地。重试次数控制在2到3次之间再多就是浪费。重试必须用指数退避第一次等1秒第二次等2秒依此类推否则故障瞬间的重试风暴会把上游打得更死。真正容易被忽略的是熔断。当模型服务连续返回5xx或者频繁超时说明上游可能已经出问题了这时候继续重试只会雪上加霜。我的做法是在客户端包一层简单的熔断器连续失败次数达到阈值直接快速失败并返回兜底文案间隔一段时间后再放少量流量试探。这个逻辑用Redis也能做但单机项目用一个进程内计数器就够了。关于重试还有一个大坑非幂等请求不能盲目重试。模型接口本身是幂等的因为你只是生成文本重试不会产生副作用。但如果你把模型调用放在一个更大的业务流程里比如“先生成回复再提交工单”那么重试就必须考虑业务层面的重复风险。正确做法是把模型生成和业务提交拆开生成失败可以重试提交必须走事务和幂等键。2.3 什么该记日志从请求到Token消耗大模型应用的日志比传统服务日志更要精心设计。因为模型输出是概率性的你不能光看状态码判断对错必须把输入的上下文、输出的完整内容、Token消耗、耗时全部记录下来才能在事后复盘“为什么那次回答这么差”。我习惯为每次模型调用打印一行结构化日志至少包含以下字段字段说明示例trace_id贯穿全链路的追踪ID8f3a2c9e1b7d4f60scenario调用场景如客服问答、总结customer_servicemodel实际使用的模型名gpt-4o-miniprompt_tokens输入Token数2845completion_tokens输出Token数512total_tokens总Token数3357latency_ms接口耗时毫秒1876temperature采样温度0.2context_shape上下文组成如“系统指令3条历史2条检索片段”sys:1, hist:3, doc:2output_preview输出内容截断前200字符您好根据您的描述您需要先…有了这些日志三个问题立刻能回答这笔用户请求花了多少成本哪个环节拖慢了响应模型到底稀里糊涂输出过什么没有这些数据优化无从谈起。我在团队里甚至强制要求新接口不打印这套日志不允许合入主干。3. 提示词和上下文的“最少够用”原则大模型应用的效果七成取决于你怎么组织输入而不是怎么堆组件。这是很多团队容易搞反的地方。他们花了大力气搭检索、建索引最后却在一个乱糟糟的提示词模板上翻车。3.1 把需求拆成最小任务单元所谓“最少够用”第一步是任务拆分。不要把“做一个智能客服”当成一次模型调用来设计那必然会导致一个巨大而脆弱的提示词。正确的做法是把任务拆成一个个可以独立验证的最小单元意图识别是一个任务提取关键信息是一个任务基于知识库生成答复是另一个任务。每个单元只做一件事提示词自然短调试也容易定位。举个例子一个工单系统的自动回复模块。如果写成一个大提示词要求模型“理解用户的情绪、判断问题类型、查询知识库、生成结构化答复”那么任何一处输出不对你都不知道该调哪部分。拆成三步以后就清晰了第一步用少量Token做一个意图分类输出只有“退款/换货/物流/其他”四个选项第二步根据意图决定是否调用检索第三步只负责用规范的语气生成答复。每一部分都可以单独测。这会让整个系统多出两到三次模型调用成本略高但换来的可维护性非常值得。而且拆分后的任务几乎都可以用小模型完成单次Token消耗大幅下降综合成本常常反而更低。3.2 上下文压缩与检索的取舍上下文越长模型输出越慢、越贵也越容易被无关信息干扰。所谓“最少够用”就是在保证答案质量的前提下把进入模型的信息量压到最小。对知识库问答这类RAG场景我通常做三层过滤。第一层是检索层的Top-K控制不贪多默认取3到5条。第二层是内容层的截断每篇文档按照与问题的相关度排序超出一定字符数的片段直接丢弃。第三层才是提示词层面的策略系统指令只写必要约束历史对话最多保留最近两轮兜底话术固定不变。这里要说一个很多人没意识到的点嵌入模型虽然能算语义相似度但它不是万能的。检索结果里的“相关”不等于“有用”经常出现语义相近但完全答非所问的片段混进来。所以Top-K宁可少取也不要无脑多取。多取几条看似全面实则是给模型添乱。我在生产环境里维护一个简单的“上下文预算”系统指令不超过200 Token 当前问题不超过200 Token 历史对话最多2轮每轮不超过150 Token 检索片段最多3条每条不超过300 Token 总输入预算不超过1800 Token一旦单次请求的总Token数超出预算宁可牺牲一点信息量也要做截断或摘要。上下文保持清爽输出稳定性和延迟都会明显改善。3.3 少样本示例怎么选脏例怎么暴露提示词里最值钱的部分是少样本示例。一个好的示例比写十句抽象的“你要专业、你要友好、你要简洁”都管用。因为模型是通过模式匹配来工作的你给它什么范例它就更倾向于输出什么风格。但示例不是越多越好。我的经验是每类场景放1到2个示例足够了多了反而会压缩其他指令的空间而且示例彼此风格不一致时会干扰模型的判断。示例的选择标准有两条一是覆盖最常见的用户表达方式而不是覆盖所有可能的极端情况二是输出格式必须严格统一让模型把“示例里的格式规范”当成一种隐含规则。脏例的问题更隐蔽。知识库检索到的文档本身可能是过时的、矛盾的、甚至包含辱骂性内容。这些内容一旦进入上下文模型会非常自然地把它们当成事实来源。因此必须在提示词里明确告诉模型检索片段仅供参考如果片段之间冲突以最新日期为准如果片段内容与安全规范冲突直接拒绝回答并转人工。同时要做离线的“脏例巡检”定期从日志里捞那些“检索得分很高但人工标记为无效”的片段反馈去修正切分和索引而不只是改提示词。4. 评估体系没有回归测试的大模型应用等于裸奔传统软件有单元测试、集成测试大模型应用也有对应的“评估体系”。没有评估你每一次改提示词、换模型、调参数都是在赌运气这次看起来变好了但你怎么知道它不是另一种偶发的错觉4.1 用真实历史问答建基线集构建评估集的第一步是从线上日志里捞真实问题。我通常的做法是随机抽取过去两周的用户提问按场景分层每层选100到200条由业务方或运营同事人工打标标注“期望的回复要点”和“禁止出现的内容”。这个集子不必很大但必须贴近真实分布。基线集要覆盖三类情况高频常见问题、边界疑难问题、完全不在知识库范围内的“踢皮球”问题。最后这类尤其重要因为它的正确答案是“我不知道已转人工”如果模型非要硬答那就是事故。我见过不少系统常见问题答得不错一碰到超纲问题就开始编造这是评估集没覆盖好的直接后果。基线集做好以后只允许通过评审会修改不能随手偷偷换题。否则就会出现“为了指标好看而换题”的作弊现象这在模型评估里太常见了。4.2 判定方式规则优先模型评估为辅回答判分是一个很麻烦的问题模型输出是自然语言没法用“断言等于”来判断。我推荐的做法是分层判定能写规则的用规则规则覆盖不了的再上模型评审不要一上来就整个“AI评测官”。规则层能判定的情况其实很多。比如要求输出必须是JSON格式那就先做JSON解析解析失败直接判负。比如“禁止出现的内容”可以用关键词表或正则匹配命中即判负。比如“答复的核心要点”可以设计成“必须包含产品名/单号/渠道”这类实体存在性检查。这些都是零成本、零延迟、完全确定性的。规则判不了的再用模型评审。评审模型和回答模型最好不是同一个避免偏好同源。评审指令也要写得极其具体不是笼统地问“这个回答好不好”而是给出参考要点清单让它逐项打分并说明理由。我习惯用0到2分制而不是百分制因为大模型判分的粒度其实很粗百分制只会制造精确的假象。4.3 每次改动跑一遍回归然后才知道是不是变好了评估集建好之后剩下的就是建立习惯每一次提示词改动、每一个检索参数调整、每一次模型版本升级都把这套回归跑完记录得分变化。这一步看起来繁琐但真的是大模型应用开发里最有杠杆效应的投资。我第一次带团队建回归时大家很抵触觉得“调个提示词还要等十五分钟”。直到有一次升级模型版本原有的检索问答得分从82降到71如果不是回归跑出来这个版本很可能就悄悄上了线然后用户在深夜遇到一堆答非所问。从那天起回归测试成了合入门禁的一部分。回归测试的另一个用途是识别“伪优化”。有时候你改了一个提示词测试集得分确实涨了但分项看是“安全拒绝”类题目涨了而普通问答类题目跌了。这种变化说明是模型学会了更保守不是学会了更准确。单独看总分会被欺骗必须分维度看趋势至少区分准确率、安全率、格式合规率和拒答准确率。5. 线上运营的稳定清单缓存、路由与人工介入模型上线只是开始稳定运营才是大头。大模型接口和普通API最大的区别在于它的成本高、延迟高、行为不确定。这三个特征决定了你不能像调普通数据库那样随意调它。5.1 结果缓存与动态开关先说缓存。对绝大多数业务场景用户的提问有很强的重复性。比如“怎么退款”“发货了吗”这种问题一天能出现几百次。如果每次都要真实调用一次模型成本高而且没必要。我的做法是维护一个基于语义的缓存先对用户问题做归一化处理再算嵌入向量在缓存里找相似度高于阈值的旧答案直接返回。但缓存要设寿命。知识库内容一更新旧答案就不能再对外输出。我在缓存键里带上知识库版本号发布新文档时直接把相关缓存全部清掉避免用户问到新政策时还拿到旧回答。同时缓存命中要打单独的日志标记方便统计命中率和成本节约效果。动态开关更像保命手段。系统的每个关键环节都要压一个配置开关检索开关、缓存开关、流式输出开关、模型路由开关。一旦线上行为异常操作同学可以一键切换。有一次模型升级后我们发现回答风格偏激进直接通过开关切回旧模型全程不到一分钟用户无感知。这种能力要在系统第一天就设计进去而不是等事故发生时再加班补。5.2 模型路由与成本平衡不少团队手里有多个模型可用从高端大模型到轻量小模型。如果所有流量都打大模型成本会非常难看如果都打小模型复杂问题又处理不好。模型路由就是为了解决这个平衡。路由策略最简单的版本是“两档路由”先用一个极小的分类模型判断问题复杂度简单问题走小模型快速回答复杂问题才转发给大模型。这种方案实现成本低收益却很大。我在一个问答类项目里测过大约60%的流量可以走小模型总成本直接降了一半多。更稳妥的路由还有“重试升级”策略优先调用便宜模型如果它对自己没信心——比如输出里包含“我不确定”“建议咨询人工”这类信号或者置信度分数低于阈值——再升级到大模型重答一次。这个策略比主动分类更容易实现因为不需要单独训练分类器代价是偶发多一次调用延迟。路由层必须记录每一次路由决策和原因方便月底复盘“为什么这个月的大模型调用占比升高了是不是新上线的文档太难小模型顶不住了”这些问题只有拿到数据才能回答。5.3 人工审核兜底和反馈闭环再成熟的模型也会犯错。因此线上系统必须设计人工兜底位置。对客服类场景我的做法是当模型判定自己“低置信度”或用户连续追问“你说得不对”时自动转人工人工端能看到模型生成草稿、参考的检索片段、用户历史记录一键采纳或修改。人工反馈是整个系统提升价值最大的数据来源。运营或客服每天修正的内容就是最真实的标注样本模型哪些地方错了哪里答偏了哪些文档没检索到。把这些数据沉淀下来定期回流到评估集再反向优化检索和提示词整个系统就转起来了。很多团队把“人工修正”当成丢脸的事觉得说明AI不行。恰恰相反有人工反馈闭环的系统越跑越准没反馈的系统永远在原地踏步。6. 一个具体案例把以上原则落地的知识库问答Demo前面讲了一堆原则可能有点干。这一节我用一个完整的模拟案例把思路串起来。场景是某公司内部客服知识库问答系统目标用户是客服坐席帮助他们快速找到制度、流程和政策答案。6.1 场景设定与范围控制需求刚提出来的时候业务方给了很长一串要支持语音输入、要自动生成工单、要实时学习新知识、要分析用户情绪。我顶着压力把第一期范围砍成了四件事客服输入问题系统返回答案并标注出处答案附带相关文档链接检索不到时明确回答“暂无答案”每次问答都记录日志和反馈按钮。第一期明确不做多轮对话不做主动推送不做多模态。事实证明这个范围控制太关键了整个项目从启动到上线只用了两周核心代码量不到一千行。因为链路短后面所有排查都能在三分钟内定位。6.2 链路实现和关键代码系统链路非常朴素用户提问 → 向量检索知识库 → 拼装提示词 → 调用模型生成 → 返回答案与出处。没有编排框架没有智能体没有工作流引擎。知识库是提前处理好的Markdown文件按标题切块后存入向量库。检索用现成的嵌入接口和向量检索库取Top 3。提示词模板固定如下你是某公司的内部客服助手。请基于下方给定的资料回答用户问题。 要求 1. 如果资料能直接回答请给出简洁准确的回答并注明引用来源编号。 2. 如果资料不足以回答请直接回复“暂无相关答案建议转人工”。 3. 不要编造资料中不存在的信息。 4. 回答控制在200字以内。 资料 [1]文档标题退款政策…… [2]文档标题配送说明…… 用户问题…… 回答就是这个简单的模板配合示例跑出来的效果比很多团队精心搭建的长链路都稳定。关键原因很简单任务单一、约束清楚、上下文干净。核心调用代码就三部分检索、拼装、生成。def build_context(query: str, top_k: int 3) - list[str]: 从向量库检索与 query 最相关的文档片段。 q_emb embedding_client.embed(query) hits vector_store.search(q_emb, top_ktop_k) context_chunks [] for i, hit in enumerate(hits, start1): # 截断过长片段避免把无关信息也灌进去 text hit[text][:300] context_chunks.append(f[{i}]文档标题{hit[title]}{text}) return context_chunks def generate_answer(question: str) - dict: context_chunks build_context(question) if not context_chunks: return {answer: 暂无相关答案建议转人工, citations: []} user_content ( \n\n.join(context_chunks) f\n\n用户问题{question}\n\n回答 ) messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_content}, ] answer, usage llm_client.complete(messages, temperature0.2) return {answer: answer, citations: context_chunks, usage: usage}这里有几个细节值得说一下。检索不到任何片段时直接返回“暂无答案”不再强行调用模型生成。因为让一个没有任何资料依据的模型硬答十有八九是在编。第二系统提示词要求回答标注引用来源编号这不是为了好看而是为了让客服能快速点开原文核对也方便出问题时定位是哪篇文档误导了模型。6.3 上线后遇到的两个真实问题这个系统上线后遇到过两个有代表性的问题第一个是“过度引用导致答案冗长”。运营反馈客服不爱用因为回答经常把三篇文档的内容全部罗列一遍看着像资料堆砌。排查发现问题出在提示词里“请给出简洁准确的回答”这话太模糊模型不知道多简短才叫简洁。后来在少样本示例里加了一条极端的对比一条引用一个出处回答只有两行另一条罗列三个出处回答五行的样子让模型直观看到“好的答案长什么样”。改完之后答案的平均长度从接近300字降到了150字左右。第二个问题是“文档更新后模型还在用旧政策”。客服问退货期限系统经常答出旧版政策。排查链路后发现问题的根子不在提示词而在检索排序旧版文档里包含大量“退货注意事项”这类与问题高度重合的词导致它一直排在新政策前面。解决的方案很直接在文档切块时给每块打上生效日期提示词里要求“优先采纳生效日期最新的资料”同时把旧策略文档移到单独的归档索引不让它与现行政策参与同一个检索池。这一步微调之后旧政策误答率基本清零。这个案例还说明一个规律大模型应用里的绝大多数线上问题根因都在工程侧——数据切分、检索排序、上下文组织、切换开关——而不是模型能力本身。这也再次印证了“把链路做简单”的好处链路短定位快修复也快。7. 我的个人体会把“简单至上”养成习惯做了几个大模型应用项目以后我最大的体会是这不是一个技术能力问题而是一个自我克制问题。每个技术点本身都不难难的是在每一个决策点都问一句——这个复杂度是非留不可的吗有没有更直接的方式我现在接手一个新需求会先写一行注释这个需求的最小闭环是什么。然后只做最小闭环其他一切后续再说。这个习惯帮我少加了很多班也让线上系统稳定得多。另一个小习惯是每周末花十分钟看日志里的异常样本你会发现很多你以为已经很稳的地方其实一直在靠运气撑着。如果只让我给一条建议那就是先跑通一个极简版本再让真实流量告诉你下一步该加什么。不要站在空地上想象架构架构是在真实数据的压力下长出来的。大模型应用开发最有意思的地方在于上限很高但最稳健的路恰恰是从最笨、最简单的那一行代码开始。
阅读完成 · 觉得有帮助?