做了一年多大模型应用开发,带过几个从零起步的项目,也带过不少刚入门的同学。我发现大家遇到的最常见问题,不是“API怎么调不对”,而是“整个应用开发的流程到底怎么串起来”。这篇就把我从入门到搞明白的完整过程整理出来,按照实际做项目的顺序,覆盖环境搭建、Prompt设计、RAG检索增强、Function Calling与Agent、微调、评估上线,以及排错经验。适合刚接触大模型应用开发的同学通读一遍建立全局认知,也适合已经写完单体Demo、正打算往工程化方向走的人查漏补缺。
1. 先想清楚:大模型应用开发到底在做什么
1.1 这活儿本质是“搭积木”
大模型应用开发,严格来说不是研究模型怎么训练出来的,而是把现成的模型能力封装成业务功能。你的核心竞争力不在于会用某个推理接口,而在于知道怎么设计Prompt、怎么管理上下文、怎么接外部工具、怎么评估效果,以及上线之后怎么稳定运营。
我经常打一个比方:大模型就像一个知识广博但记性很差的外包员工。它懂很多通用知识,但对你的业务细节一无所知;它能听懂指令,但你必须把指令说得足够清楚;它不会主动去查数据库、调系统,但如果你给了它“双手”(工具调用),它能指挥你把活干完。应用开发者的活,就是当好这个外包员工的“项目经理”。
理解这个本质之后,很多迷茫就消失了。你不需要焦虑自己数学基础不够好、不懂模型内部原理,就能做出有价值的应用;但你也别以为写两个Prompt就完事了,真实业务里的稳定性、成本、数据隐私、效果评估,每一项都比“让模型说出一段漂亮话”难得多。
1.2 两条路线怎么选:商业API还是开源部署
动手之前,先选路线。目前主流有两条:调用商业API,和本地部署开源模型。两者各有明确适用场景,不搞清就动手,后面很容易返工。
调用商业API的好处是上手快、链路稳定、按量付费,不需要考虑显卡和运维。适合大多数业务场景,尤其是团队小、迭代快、对数据不敏感的情况。代价是数据会经过第三方服务,隐私敏感的行业需要评估合规风险;另外随着调用量上涨,成本控制是个长期课题。
本地部署开源模型的好处是数据不出内网、可以任意微调、长期成本可能更低。代价也很直接:你得有两块像样的显卡,得处理推理框架、并发排队、版本升级这些问题。适合对数据安全要求高的场景,或者业务本身重度依赖本地结构化数据。
我的建议很简单:没到非自部署不可的程度,先用商业API把业务跑通,等验证了需求、摸清了调用量和成本,再决定要不要迁移到自部署。这就跟打车和买车的关系一样,先别急着贷款买跑车。
1.3 一个标准应用的基本骨架
不管你做什么具体业务,大模型应用的基本骨架都绕不开几个环节:输入处理、Prompt构建、模型推理、输出解析、外部检索、工具调用、记忆管理、效果评估。
输入处理解决的是用户消息怎么清洗、怎么脱敏、要不要分类;Prompt构建是把业务要求翻译成模型能理解的指令;模型推理就是真正花token的那一下;输出解析则是把模型的文字结果转成结构化数据,该转JSON转JSON,该校验校验;外部检索解决知识从哪里来;工具调用让模型能操作业务系统;记忆管理负责处理多轮对话和长上下文;效果评估则回答“这么做到底行不行”。
我第一次做项目时,只写了“Prompt+模型调用”就以为完事了,结果线上问题一堆:模型偶尔输出格式不对、知识库内容检索不到、用户多问两轮就“失忆”。后来才发现,那些被忽略的环节才是真正决定应用能不能落地的部分。把这个骨架记在心里,之后每一步都是在给它填肉。
1.4 从入门到精通的分阶段路线
总结我带人的经验,大模型应用开发基本要经历五个阶段。
第一阶段是跑通最小闭环:搭好环境、能调用模型、写一个最简单的对话Demo,搞清楚请求和响应的结构。第二阶段是Prompt工程入门:能把模糊需求写成结构化指令,控制输出格式,让模型稳定完成单一任务。第三阶段是上下文与RAG:学会管理多轮对话,给模型外接知识库,让它回答原本不知道的问题。第四阶段是工具调用与Agent:让模型操作数据库、API、业务流程,从“会说话”变成“会干活”。第五阶段是微调与工程化:用微调解决Prompt解决不了的问题,同时搭好评估、监控、缓存、安全体系。
每个阶段都有关键技能要补。别跳阶段,尤其是别没做好Prompt和RAG就直接冲微调,微调不是灵丹妙药,后边我会专门讲。
2. 环境准备与模型接入:从拿到Key到跑通第一个Demo
2.1 开发环境:Python、虚拟环境、依赖
做这块开发,基本都绕不开Python。建议直接用Python 3.9以上版本,太低版本会遇到依赖兼容问题。装好Python之后,给每个项目单独建虚拟环境,这是避免依赖冲突最简单的手段。
python -m venv .venv source .venv/bin/activate # Windows下是 .venv\Scripts\activate pip install requests很多人图省事,直接全局装包,等几个项目挤在一起就开始互相踩依赖了。虚拟环境是花十秒钟省一整天的事,强烈建议养成习惯。刚开始开发,依赖不用装太多,一个requests就足够跑通流程了,等后面用到向量库、框架再逐个加。
API Key建议放到环境变量或者本地配置文件里,不要commit到代码仓库。我见过不止一次有人把Key写死在代码里然后传到公共仓库,几分钟内就被外部脚本扫走盗刷。
2.2 最简接入:写一个能对话的函数
现在主流模型的API格式都很接近,基本都采用“messages列表”结构,区分system、user、assistant三种角色。这是整个大模型应用开发里最核心的交互模型:system负责设定身份和规则,user是用户输入,assistant是模型回复,多轮对话就是把历史消息按顺序追加进这个列表。
下面这个函数是我每开一个新项目都会先写一遍的最小骨架,直接能跑通:
import requests import os API_URL = "https://api.example.com/v1/chat/completions" API_KEY = os.environ.get("LLM_API_KEY") def chat(system_prompt, user_message): resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": "your-model-id", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], "temperature": 0.3, }, timeout=30, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] if __name__ == "__main__": print(chat("你是一个严谨的技术助手", "请用三句话解释什么是RAG"))注意这里故意没有用任何第三方封装库,因为requests能看到最原始的请求和响应结构,对理解整个链路最有帮助。等跑通了,再考虑用各平台自己的SDK或者框架封装,都是水到渠成的事。
2.3 必须搞懂的四个交互参数
调用模型时,有四个参数几乎每次都会用到:temperature、top_p、max_tokens、model。
temperature控制随机性,取值一般在0到1之间。数值越低输出越稳定、越确定,适合代码生成、信息抽取、格式转换这类任务;数值越高越发散、越有创意,适合文案写作、头脑风暴。我的经验是:需要复现的场景尽量压在0.2以下,需要创造力的场景才放到0.7以上。网上很多“模型输出飘忽不定”的问题,第一排查对象就是temperature没调低。
top_p是核采样参数,作用是控制候选词集合大小。现在很多平台推荐只调temperature或者只调top_p,两者不要同时猛调,否则输出会变得很难预测。我基本固定top_p为默认值,只动temperature就已经能覆盖大多数场景了。
max_tokens控制生成的最大长度,超了就截断,常常表现为“话说到一半断了”。设置时根据任务内容估算,不是越长越好,因为生成长度直接决定成本和延迟。
model是模型选择,同一个API下往往有多个规格。开发阶段用大模型调试,上线时可以考虑“简单任务路由到小模型”,这个后边在成本控制部分细聊。
2.4 接入阶段最容易踩的坑
接入阶段我踩过的最典型的坑有四个。
第一个是超时。线上模型推理慢的时候,几十秒不返回也是正常的,如果你没设timeout,请求就会一直挂在那里,拖死应用线程。设了timeout,又要考虑重试策略:对网络抖动导致的小概率失败,通常重试2到3次,每次退避递增,效果就很好了。
第二个是限流。API基本都有每分钟调用次数限制,并发一高就会收到限流错误。这时候别死磕,做足重试退避,同时在应用层加信号量控制并发,比单纯靠库的重试机制可控得多。
第三个是HTTP状态码之外的业务错误。有些平台返回200,但body里带着错误码,直接解析choices就可能KeyError。健壮写法是先看返回结构里有没有error字段,再做响应解析。这个坑不遇到一次,很难想起来防御。
第四个是消息结构的细节。比如有些平台要求assistant消息不能连续出现,有些平台对messages列表总长度有硬限制。调多轮对话时尤为明显,后面上下文管理专门讲。
3. Prompt工程:决定应用上限的隐藏工程
3.1 为什么Prompt本身就是工程
很多人觉得Prompt就是“写几句话让模型干活”。真做起来你会发现,同样的需求,Prompt写得好不好,效果差距能到天壤之别。原因在于大模型本身不读心,它只能根据你给的信息做大概率补全。指令模糊,它就按自己最舒服的方式猜,猜的结果往往不是你要的。
Prompt工程的核心目标只有一个:降低模型“猜错”的概率。办法无非是把任务目标、背景信息、输入材料、输出格式、约束条件全部讲清楚。你越想让它稳定干活,指令就越不能只靠自己“感觉说清楚了”,而是要写到换一个人来读也不会理解错的程度。
给个直观对比。弱Prompt是:“帮我写一个活动通知。”模型到底写校园活动、促销活动还是技术沙龙?语气正式还是轻松?要不要给具体时间地点?全是猜。强Prompt则会把场景、目标、要点、语气、长度、格式一一写明,模型几乎没有自由发挥余地,自然更稳定。
3.2 一套可以直接抄的结构化模板
我实际项目里一直在用一套模板,核心结构是五段式:任务定义、背景说明、输入数据、输出要求、约束条件。
任务:根据用户评论生成一条客服回复草稿。 背景:我们是一家提供在线存储服务的公司,回复需要维护品牌形象,同时解决用户疑虑。 用户评论:{这里放用户评论原文} 输出要求: 1. 以“感谢您的反馈”开头。 2. 语气温和,不得承诺无法兑现的事项。 3. 全文不超过80字。 4. 直接输出回复内容,不要输出任何解释。这套模板看起来简单,但每个部分都有实际作用。任务定义让模型知道“做什么”,背景让模型知道“以什么身份、为了什么目标做”,输入数据是原料,输出要求定义了考核标准,约束条件堵住了模型想自由发挥的漏洞。真正遇到复杂任务时,每个部分还可以再拆细,比如背景里加用户画像,约束里加禁用词列表。
写Prompt有个很实用的自我检查方法:把模板里的模型替换成实习生,想象一下你把这个需求交给一个靠谱但完全不了解前因后果的实习生,他能交回来合格的活吗?如果不能,就是Prompt还不够清楚。
3.3 few-shot与思维链:小投入大回报
结构化模板之上,提升效果最明显的两个技巧就是few-shot和思维链。
few-shot就是在Prompt里给模型几个输入输出示例。模型虽然不能真正“理解”示例,但示例能极大地约束输出风格和格式。比如你要求模型把用户问题分类成“售前、售中、售后、其他”四类,光写一句“请分类”不够稳,但你给两个精确的分类例子,再让它分,准确率立刻上一个台阶。示例数量不用多,我常试2到5个,覆盖典型情况和边界情况就够了。
思维链(Chain-of-Thought)是要求模型在做复杂推理时先把推理过程写出来,再给最终答案。典型例子是数学题、多条件判断、需要逐步拆解的任务。直接问“这个任务能不能安排在今天”,模型可能跳步;如果要求它“先列出任务清单、估计每项耗时、再判断是否排得开”,答案的可靠程度会明显提升。
思维链不一定非要把“我们一步步思考”这句话写出来,更有效的方式是在任务说明里直接要求“给出计算过程,再在最后一行输出结论”。这样既能拿到推理依据,又方便后一步解析结果。不过要注意,简单任务不要强行加思维链,否则白白增加token消耗和延迟,收益却不明显。
3.4 输出格式管理的实战技巧
业务系统要消费模型的输出,就不能容忍模型自由发挥,必须把输出格式牢牢钉死。最常用的做法是要求JSON输出。
直接说“输出JSON”往往不够稳,模型的JSON里可能带markdown代码块标记、可能字段名跟你要的对不上、可能夹杂解释文字。更靠谱的写法是把期望的JSON结构直接写进Prompt,附上示例值,并加上“只输出JSON对象,不要输出解释”。比如:
请从用户问题中提取关键信息,输出JSON对象,字段如下: { "intent": "query_order | cancel_order | other", "order_id": "字符串,如果没有则为null", "needs_human": "布尔值,是否必须转人工" } 只输出JSON,不要输出任何其他内容。即使这样,仍不能100%保证模型按格式输出。所以在代码侧要做两件事:第一,解析时用宽松策略,比如先把markdown代码块标记剥掉,再用正则从文本里抽第一个JSON片段;第二,解析失败时重试一次,把“上次输出格式错误,请严格按照要求重新输出”作为追加消息传回去。这套组合拳,能让格式稳定性从90%拉到99%以上。
温度参数在这里也是关键。凡是涉及输出解析的任务,temperature直接调到0到0.2之间,能明显减少格式飘忽。
4. 上下文与RAG:让模型查得到、记得住
4.1 先理解模型的“记忆力”到底有多差
大模型虽然看起来知识渊博,但存在两个硬伤。第一,它的训练知识有截止时间,最新的业务信息、内部文档、实时数据它一概不知;第二,它天生有幻觉倾向,不知道答案的时候不会老实说“不知道”,而是会编一个听起来合理的答案。
所以想让模型回答业务问题,必须把相关资料主动送到它眼前。RAG(检索增强生成)就是干这件事的:先从一个知识库里检索出跟用户问题最相关的内容,再把检索结果拼进Prompt,让模型基于这些内容回答。说白了,就是把模型从一个什么都硬答的“半吊子”变成“开卷考试的考生”,资料由你给,它负责阅读和作答。
我一开始做知识库问答时天真地以为“把文档全部塞进Prompt就行”,结果几百页文档一塞,要么超长被截断,要么模型抓到一堆无关内容开始胡说。后来才意识到,RAG的关键不只是“塞进去”,而是“怎么塞得少而准”。
4.2 RAG的完整流程与核心环节
一个标准的RAG流程大致分成离线索引和在线检索两段。
离线索引阶段:把原始文档清洗干净,去掉页眉页脚、无关广告、乱码,然后按策略切分成小块,每一块通过Embedding模型转成向量,存入向量数据库。在线检索阶段:把用户问题也转成向量,在库里搜索最相似的若干文本块,再把这些文本块作为上下文,连同原始问题一起发给模型生成回答。
整个链路里最影响效果的有三处:切块策略、Embedding模型、检索后处理。
Embedding负责把文本映射成一个高维向量,语义相近的文本在向量空间里距离更近。选Embedding时重点看两点:一是有没有针对中文优化过,直接拿英文预训练的向量模型跑中文,效果通常打折扣;二是对比检索召回样例,不要只看榜单分数,实际业务里“相近语义”的定义很个性化,跑一跑自己数据最有说服力。
向量数据库的选择则看规模。实验阶段只有几千个块,直接写个脚本加载到内存里用numpy算相似度就完全够用;到了生产级别,再用专门的向量库,重点关注是否支持混合过滤、索引构建速度和查询延迟。
4.3 切块策略怎么选
切块是整个RAG里最容易被轻视、又最容易翻车的地方。切大了,一块内容包含多个主题,跟问题相关的部分被稀释;切小了,语义不完整,单块信息量不足,模型也答不利索。
我用的经验值是这样的:常规文档按固定长度切512个token左右,相邻块重叠80到100个token,防止上一块末尾的语义被腰斩。代码或日志这类结构性强的内容,优先按函数、模块、报错段落切,一个完整单元算是天然的好块。Markdown、HTML这类带标题结构的文档,按标题层级切分效果往往比纯固定长度好很多。
三者的对比我整理成了表格:
| 切块方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度+重叠 | 简单通用,实现成本低 | 可能切断语义 | 通用文档、百科类内容 |
| 结构切分 | 语义完整,贴合原文 | 块大小不均 | 代码、Markdown、HTML、合同条款 |
| 语义切分 | 按语义边界切,质量高 | 依赖额外模型,成本高 | 长报告、会议纪要、问答对 |
切块这块没有绝对标准,最好的办法是拿真实业务文档,配合测试问题做对比实验。你会很快发现,某些问题检索不到,改改切块策略就好了,根本不用换Embedding模型。
4.4 检索优化:从“能搜到”到“准确”
向量检索不是银弹,典型场景就经常翻车:用户问“订单号ABC123有没有发货”,向量检索会把“订单号ABC123”这种精确匹配需求当作语义相似度来处理,容易匹配到完全不相关的句子。
解决办法是混合检索:把关键词匹配(比如BM25)和向量检索的结果合并,再做融合排序。关键词擅长精确匹配,向量擅长语义扩展,两者结合后,既能听懂“发货了没有”这种口语问法,也不会放过“ABC123”这种精确ID。
检索质量的重排环节同样关键。向量检索先召回一批候选(比如50个),之后可以用更重的重排模型对候选按相关性重新打分,只取前5个作为最终上下文。这一步能把大量“搜到了但不相关”的结果过滤掉,模型生成质量立竿见影。重排虽然增加了一点延迟和成本,但相比整体效果提升,这投入非常值。
还有一个小经验:检索结果数量不要盲求多。每一块可能几百个token,5块就是一两千token,如果大部分无关,模型就容易被带偏。检索结果宁缺毋滥,给模型喂3到5块高质量内容,比喂10块乱七八糟的内容效果好得多。
4.5 把检索结果拼进上下文
检索到内容之后,拼Prompt也有一些讲究。我习惯用清晰的标记把检索材料和其他指令分隔开,比如“以下是从知识库检索到的参考资料,用 编号 标注:”。然后在回答要求里明确:“只能依据参考资料回答,如果资料没有提到,直接回答‘根据现有资料无法确认’,不要编造。”
这里有个反直觉的细节:参考资料里如果混入了明显与问题无关的内容,模型被带偏的概率会急剧上升,因为它倾向于“凑合所有输入”。所以拼接前要做一次质量过滤,比如相关性得分低于阈值的直接丢弃。
另外要盯紧上下文总量。不同模型对上下文长度有硬限制,超出部分会被静默截断。一旦系统指令被截断,模型行为立刻崩坏。我的习惯是做一个简单的token估算函数,每次拼接后算总量,超出限额就优先丢弃最老的对话历史、而不是压缩参考资料。参考资料是当前问题的“考卷”,对话历史只是辅助,保前者丢后者。
5. Function Calling与Agent:让模型从聊天变成干活
5.1 工具调用解决的问题
大模型本质上只能“生成文字”,不能真去查你的订单表、不能真去改数据库、不能真去发邮件。但如果你定义一个函数列表告诉它:“当用户需要查订单时,请输出一个结构化的调用请求,参数包括订单号”,模型就能按要求输出这个请求。应用层收到请求后调用真实接口,拿到真实结果再交回给模型,模型据此生成对用户的回答。
这就是Function Calling(工具调用)的基本机制。它把模型的“语言理解能力”和外部系统的“真实执行能力”连接起来,是大模型从玩具到生产力的关键一步。
我印象最深的一个场景是给内部运维做辅助机器人。用户问“线上服务是不是挂了”,模型通过工具调用去查监控接口,拿到实际状态之后才回答“目前有3个节点异常”。没有工具调用时,模型只能凭感觉说“建议检查服务”,那种回答本质上就是空洞的废话。
5.2 函数定义与调用流程
工具调用的核心是函数描述。你需要用一个结构化的schema告诉模型:函数名叫什么、有哪些参数、参数类型是什么、这个函数是干什么的。以订单查询为例:
{ "name": "query_order", "description": "根据订单号查询订单状态和物流信息", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "用户提供的订单号"} }, "required": ["order_id"] } }调用流程是一个多轮交互,而非一次请求就完事:用户提问,模型分析后认为需要调用query_order,于是在回复中携带工具调用请求;应用层解析这个请求并调用真实接口,拿到结果后作为一条新消息回传给模型;模型看到工具结果后,生成对用户的最终回答。
这个流程在整个Agent应用里会不断循环,每一次循环都要维护好messages列表。工具调用的返回结果尽量结构化,比如直接返回JSON字符串,别让模型去解读一团乱麻的文本日志。日志类数据尤其要提炼,只给模型看关键字段。
5.3 Agent循环与防失控设计
当一个模型可以连续多次调用不同工具时,就进入了Agent的范畴。经典的Agent循环是“思考、调用工具、观察结果、再思考”,直到模型认为任务完成,给出最终答复。
这个循环设计上有几个容易失控的点。第一是无限循环,模型因为拿不到理想结果不断重试同一个调用,token飞快消耗。第二是参数幻觉,模型调用了一个不存在的函数名,或者编造了一个参数,应用层解析直接出错。第三是后果不可控,工具带有修改功能的副作用,模型在误导下执行了不该执行的操作。
我的防失控设计有三板斧:一是限制最大迭代次数,一般设为3到5次,超过就强制结束并转人工兜底;二是做函数白名单校验,模型要求调用的函数必须存在于注册表里,参数也要做类型校验,非法参数直接拒绝;三是所有带写操作的工具默认加“二次确认”开关,对风险操作要求用户确认后再执行。
5.4 一个订单查询助手的实现思路
用一个具体例子把上面串起来。假设要做一个订单查询助手,配置两个工具:query_order查订单,send_coupon给用户发补偿券。代码骨架大概是这样:
messages = [{"role": "system", "content": "你是订单客服助手。查询订单后如实回答,不要编造信息。"}] messages.append({"role": "user", "content": "我的订单A123还没发货,帮我看看"}) for _ in range(5): response = call_model(messages, tools=TOOL_SCHEMAS) msg = response["message"] if msg.get("tool_calls"): messages.append(msg) for call in msg["tool_calls"]: result = execute_tool(call["function"]["name"], call["function"]["arguments"]) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False) }) else: final_answer = msg["content"] break这里有个我特别想提醒的细节:工具执行的返回结果,要当作“事实依据”提交给模型,而不是直接拼进用户消息里。协议上通常会区分“工具返回消息”和“普通对话消息”,写代码时别图省事把工具结果塞进user消息,那样在多轮工具调用场景下,模型容易把历史工具结果和用户最新意图混在一起。
另外,参数解析要容错。模型偶尔会把参数写成JSON字符串、甚至带注释的伪JSON,解析失败时先做一轮清理,比如去掉注释、修正多余的逗号,再重试。解析失败太多次就放弃本轮工具调用,直接回复“我需要人工介入”,比硬撑更体面。
6. 微调:什么时候做、怎么做、怎么评估
6.1 别急着微调,先判断有没有必要
微调是在模型原有能力基础上,用一批高质量数据做进一步训练,让模型在某些任务上表现更好。很多同学一觉得效果不好就想着微调,这是成本最高、风险最大的冲动。
在动手微调之前,建议先把这个判断矩阵过一遍。如果你要解决的是“模型不知道内部知识”的问题,优先RAG,微调不适合存储碎片化事实;如果你要解决的是“输出格式不稳定”的问题,优先Prompt约束加解析兜底,微调不是唯一解;如果你要解决的是“必须完全按照某种话术/风格输出”或者“模型在某个专业领域理解明显不足”,且Prompt和RAG都试过仍不满意,这时候才轮到微调。
再补充一个常见动机:数据不能出内网。这种情况下你没法用商业API,只能本地部署开源模型,再通过微调让模型适配业务。这类场景里微调不是优化手段,而是方案前提。
微调和RAG从来不是互斥的。实际项目里两者常常配合:微调负责让模型适应专业术语和输出风格,RAG负责提供最新的事实材料。我印象比较深的某个业务助手就是这么做的,模型先经过微调学会规范话术,再通过RAG获取实时库存和价格信息,两边各司其职。
6.2 微调数据怎么准备
微调效果七分靠数据,三分靠参数。数据质量不过关,再怎么调参都是白搭。主流微调格式就是对话格式,一条样本对应一轮完整的对话:
{ "messages": [ {"role": "system", "content": "你是订单客服助手,回答简洁,不得编造信息。"}, {"role": "user", "content": "订单A123什么时候发货?"}, {"role": "assistant", "content": "您的订单A123已于今天上午10点发货,预计明天送达。"} ] }数据数量上,几百到几千条高质量样本足够撬动明显变化,不用一上来就追求几万条。数据质量上,每条都要检查有没有错误答案、有没有不符合目标风格的表达,一个错误样本会造成模型反复学习到不想要的行为。
我踩过最深的坑是数据全部来自模型自己生成的答案,没做人工质检就拿来训练。结果模型把“胡说”当成正常行为学得更到位了,线上效果不升反降。从那以后,我坚持每条训练数据的assistant回答至少有一个人工复核节点,宁可少训练一千条,也绝不带病上船。
6.3 LoRA参数与训练实操
对大多数团队来说,全参微调既烧资源又难控制,通常用LoRA就够了。LoRA的基本原理是冻结原始模型权重,只在旁边训练一个低秩矩阵,训练参数量骤降,显存需求大幅减少。
LoRA有四个参数需要特别关注:lora rank(秩)、学习率、训练轮数、批次大小。我的经验范围是:
| 参数 | 推荐区间 | 我的用法 |
|---|---|---|
| LoRA rank | 8到32 | 先试8,数据量大或任务复杂再加 |
| 学习率 | 1e-5到3e-4 | 大多任务用2e-4附近起步 |
| 训练轮数 | 1到5 | 先3,看验证loss再调 |
| batch size | 4到8 | 依显存来,小显存用梯度累积 |
训练过程中要盯住loss曲线,同时准备一组与训练数据不重叠的验证问题,在每个epoch结束后跑一遍。训练不足,任务表现提升不明显;训练过头,模型可能“遗忘”原有通用能力,连日常对话都变得僵硬。这个平衡点没有固定答案,只能靠验证集实测判断。
顺带提醒一点:LoRA训练完要测试“微调前和微调后的同一批问题”的效果差异,因为正常情况下,训练loss下降是必然的,但业务指标是否提升需要单独验证。模型在训练集上表现好,不代表在真实用户提问上表现好。
6.4 微调完怎么评估
微调后的评估坚决不能只看“感觉变强了”。我见过很多人微调完让同事随口聊两句就说“效果可以”,结果一上线发现特定类型的问题崩了一大片。必须建一套简单的评估集再下结论。
评估集建议分三块:典型主场景样本占六成,覆盖用户最常见的问题;边界条件占两成,比如空输入、超长输入、带敏感词输入;历史坏case占两成,把微调之前表现不好的真实案例放进去,专门验证这些坑有没有被填上。
跑完评估后,逐条对比微调前和微调后的输出,记录结构化指标,比如格式通过率、关键字段正确率、拒绝回答的合理性。如果发现微调后把某些不该拒绝的问题也拒绝了,说明数据里“不知道就拒绝”的样本太多,需要减少拒绝样本比例或降低训练轮数。
7. 评估与工程化:上线前的最后一公里
7.1 没有评估集,所有优化都是盲人摸象
做一段时间大模型应用后,你会发现一个残酷事实:每次改Prompt或者换模型,都会有一批老case变好、另一批变差,如果不记录,你根本分不清整体是在进步还是倒退。所以我给所有项目定的规矩是:上线前必须有评估集,哪怕一开始只有几十条。
评估集怎么建?把真实用户问题按场景分类收集起来,挑有代表性的,再补充一些极端情况和历史出过错的问题。指标不要只盯着一个,我常用这五类:正确率(答案对不对)、格式通过率(结构和字段是否合规)、拒绝率(该说不知道时有没有乱说)、延迟、token成本。
有了评估集,每次修改都在同一批问题上跑一遍,记录分数变化,你就能清楚看到改动带来的真实影响。这个习惯能把“我觉得”“大概”“好像”全部替换成数字,团队协作时尤其有用。
7.2 日志、缓存与并发控制
线上系统出了坏case如果没法重放,定位问题就成了一场灾难。所以从第一天开始就要记录完整请求日志:系统Prompt、用户输入、上下文拼装内容、模型响应、token用量、耗时、工具调用轨迹,缺一不可。
日志要特别注意上下文拼装内容的记录,很多“线上答非所问”就是检索结果或者上下文被截断导致的,不记录完整输入你就没法重演。记录时做好敏感信息脱敏,手机号、身份证号、密钥这类字段先打码再落库。
缓存是降本增效最直接的手段。大模型对完全相同的输入加上相同参数,输出不一定完全相同,但很多业务场景可以接受短时间内的相同回答。对高频且稳定的请求做缓存,命中一次就省一次推理费用。简单场景用内存LRU字典就够了,分布式场景可以再上外部缓存组件。
并发控制别忽略。API有速率限制,本地部署模型有GPU算力限制,无限发请求的结果就是雪崩。应用层要加并发信号量,超过阈值时排队或者直接返回降级提示,同时配合前面说的重试退避。这里的原则是:宁可让用户等一秒,也不要让系统在高峰时全线崩溃。
7.3 成本控制与模型路由策略
大模型应用的成本大头在推理token,而且输出token通常比输入token更贵。控制成本有几个立竿见影的手段。
第一是压输出长度。很多任务其实不需要长篇大论,在Prompt里直接限定字数,能省不少token。第二是上下文瘦身。多轮对话里历史消息长期堆积会非常烧钱,通常做法是滑动窗口,只保留最近几轮,历史内容做摘要压缩。第三是路由策略:简单任务用便宜的小模型,复杂任务才用能力更强的大模型。可以先让模型或规则做一次意图识别,判断难度后分配模型。
路由策略的写法不难,核心就是加一个判断:
def route(user_message): if is_simple_intent(user_message): return "cheap-small-model" return "strong-large-model"难点在于“简单”的定义。我的做法是先用一段时间双模型并存,把每个请求同时发给大小两个模型,对比输出质量和成本,积累数据后就能总结出路由规则。上线后再持续监控不同路由的坏case率,动态优化阈值。
7.4 幻觉缓解与内容安全
幻觉是大模型天然的缺陷,工程上要层层设防。最便宜的一层是Prompt规则,强制要求“没有依据时直接说不知道”;第二层是检索引用,回答必须标注引用的知识库来源编号,没有编号的回答判为不合规;第三层是置信度策略,对检索分数低于阈值的问题直接拒绝作答并引导用户补充信息。
还有一种幻觉来自模型“过度配合”。用户问“我能不能不付款就下单”,模型为了讨好用户可能给出建议。这类问题光靠Prompt压不住,要在应用层做输入输出过滤:输入侧识别高风险意图,输出侧再做一次内容校验,两边都拦住才安全。
内容安全上还有两件事要提早规划:敏感数据的脱敏处理和用户隐私保护。日志、向量库、缓存里都可能留存用户数据,脱敏和证书管理要有明确方案。这块没有统一模板,但晚做比早做代价大得多。
8. 高频问题排查实录
8.1 问题速查表
下面这些是群里同学和项目里新人问得最多的问题,我整理成了一张速查表,基本覆盖了从开发到上线的大部分坑。
| 现象 | 可能原因 | 解决建议 |
|---|---|---|
| 输出偶尔带markdown代码块 | Prompt没强约束,或temperature太高 | 加“只输出JSON,不要代码块”,temperature调到0 |
| 多轮对话后模型“失忆” | messages太长被截断,或历史未滑动窗口 | 记录上下文占用,优先保留system和最近轮次 |
| 检索结果相关但回答不对 | 参考资料混入无关内容,或模型没被要求只依据资料 | 提高检索阈值,重排,明确要求“资料未提及则说不知道” |
| 工具调用死循环 | 没有最大迭代限制,或参数解析失败导致重试 | 加MaxIterations,参数校验和容错解析 |
| 微调后通用能力下降 | 训练轮数过多或数据分布单一 | 减少epoch,微调后用通用能力抽查集验证 |
| 每次问同一问题答案都变 | temperature太高 | 降低temperature,对稳定场景加缓存 |
8.2 四个我实际遇到的排错案例
第一个案例是JSON输出飘忽不定。线上客服分类功能偶尔返回带markdown标记的JSON,解析直接失败。排查时发现我虽然写了“输出JSON”,但没写“不要代码块”,且temperature设成了0.7。修复方式是强约束+0.2温度+解析失败重试,问题当天就消失了。
第二个案例是用户多轮提问后答非所问。看日志发现用户发了5轮消息后,messages长度已经超过模型上下文限制,早期的系统指令被静默截断。修复方案是先压缩历史对话为摘要,再拼上当前问题和检索资料,确保系统指令永远不被挤出上下文窗口。
第三个案例是RAG检索出大量无关内容,模型被带偏。用户问“怎么退款”,检索匹配出来的段落五花八门,模型把售后政策和会员规则混在一起瞎答。修复方式是做了重排,只保留真正高相关的3到5个段落,同时给Prompt加上“如果资料不相关,直接说无法确认”。
第四个案例是Agent无限循环烧了几千个token。排查发现模型在某次工具调用时把参数解析成了非法JSON,应用层重试了一次又失败,模型陷入“继续尝试”的死循环。修复方式有三个:最大迭代限制、工具参数严格校验、解析失败超过两次强制结束转人工。
最后分享一句我自己的体会:大模型应用开发,真正拉开差距的往往不是“谁调模型调得更花哨”,而是“谁能在问题发生时更快定位根因”。上面这些坑,几乎每个都能靠日志和评估集提前发现。所以无论项目多急,别把日志和评估砍掉。你省下的那两天迭代时间,迟早会在线上故障里连本带利还回去。