1. 先把大模型这件事想明白,别急着装环境
1.1 大模型到底是个什么东西,一句话说透
很多人第一次打开对话框,输入“你好”,看到屏幕上哗哗往回蹦字,觉得挺神奇,然后就卡住了——不知道该拿它干嘛。我见过太多人停在这一步,收藏了一堆教程,环境一个没装,模型一个没跑。
要跨过这道坎,先得在心里建立一个准确的模型。大模型本质上是一个“读过海量文本的接话机器”,它做的事情只有一件:根据前面已有的所有文字,预测下一个最可能出现的词。就这么简单,没有魔法。你问它“中国的首都是”,它算出“北京”这两个字出现的概率最高,于是输出。它一个字一个字往外蹦,蹦到最后看起来就像在跟你对话。
这个底层认知极其重要,因为它直接解释了大模型的三个“性格缺陷”。第一,它没有记忆。你和它聊了半小时,关掉窗口再开一个新的,它完全不记得你是谁。所谓“记忆”,只是当前这轮对话的上下文还留在它的视野里,超出这个范围它就断片了。第二,它不会查资料。它脑袋里只有训练时“读”过的那些东西,有个截止时间点,之后发生的事它一概不知,你公司内部的文档它更不可能知道。第三,也是最坑的一点——它不知道自己不知道。它被训练成“永远给出一个像模像样的回答”,所以当它不懂的时候,它不会说“我不知道”,而是编一个听起来特别合理的答案给你。业内管这叫“幻觉”,这是后面所有工程手段要对付的头号敌人。
衡量一个模型,日常最该关注三个指标。参数量决定它的“脑容量”,常见的有 7B、13B、70B 这种量级,B 是十亿的意思。上下文窗口决定它一次能“看见”多少字,早期是 4K,现在动辄 32K、128K。训练数据决定它的语感偏向哪种语言、哪个领域。这三个指标基本框定了一个模型的能力上限和硬件成本,后面选型绕不开。
1.2 它擅长什么、不擅长什么,心里得有本账
我常跟刚入门的朋友说,别把大模型当“万能答题机”,把它当成一个“反应极快、知识面广、但有点爱吹牛的实习生”来用,你的预期就对了。这个实习生能一分钟读完五十页文档并给你摘要,但你不会让他直接去签合同。
它真正强的地方在语言类任务:改写润色、长文摘要、信息抽取、分类打标、翻译、代码骨架生成、头脑风暴。这些任务的共同点是——答案不唯一,只要“合理”就行。你让它把一段口语化的会议记录整理成结构化纪要,它干得比人快十倍。
它弱的地方也很明确:精确计算、实时信息、私有知识、严格的多步逻辑推理。你让它算 1234 乘 5678,它可能给你一个特别像那么回事的错误答案,因为它是在“猜数字”而不是“做乘法”。你问它今天天气,它只能编。你问它公司报销流程第几条,它更编。
| 任务类型 | 大模型表现 | 背后的原因 | 该怎么补 |
|---|---|---|---|
| 摘要、改写、翻译 | 优秀 | 纯语言变换,训练数据充足 | 直接上,注意长度控制 |
| 分类、打标、抽取 | 良好 | 模式匹配能力强 | 给足示例,固定输出格式 |
| 代码生成 | 中等偏上 | 见过大量代码,但不懂真正运行 | 人工审阅,配合单元测试 |
| 数学计算 | 较差 | 靠概率猜而非运算 | 让它输出算式,交给代码执行 |
| 实时信息 | 不可用 | 训练数据有截止时间 | 接入搜索或数据库 |
| 私有知识问答 | 不可用 | 从没见过你的文档 | 用检索增强(RAG)喂资料 |
| 长链条逻辑推理 | 不稳定 | 容易中间步骤出错并放大 | 拆成多步,每步验证 |
这张表建议你贴在显眼的地方。我踩过的最大的坑,就是早期拿它去做一份需要精确到小数点后两位的财务测算,输出看着规规矩矩,数字全是错的,差点出事。从那以后我的原则就一条:凡是答案必须“精确”的场景,大模型只负责生成过程,最终结果交给确定性代码去算。
1.3 从零到精通,路径其实就三段
网上教程最大的问题是把“用”和“造”混在一起讲,零基础的人一上来就被 Transformer 架构、注意力机制公式劝退。我建议按三段走,每一段都有明确的产出物,看得见摸得着,不容易半途放弃。
第一段是“会用”。目标是你能让模型稳定地按你要求干活。产出物是一套自己攒的提示词模板库,覆盖你日常最高频的十几个任务。这一段不需要写代码,一周内就能有明显手感。
第二段是“会接”。目标是通过接口调用,把模型嵌到你自己的脚本或小工具里,让它变成自动化流程的一环。产出物是一个能跑通的小应用,比如批量处理文档的脚本。这一段开始涉及代码,但对编程基础要求不高,会改参数、看懂循环判断就够。
第三段是“会调”。目标是让它回答你私有的、专业的知识,并且答得准、答得可追溯。产出物是一个内部知识问答 Demo,或者一个经过微调的专用模型。这一段才需要真正理解检索、嵌入、微调这些概念。
| 阶段 | 核心目标 | 需要的前置知识 | 典型产出物 | 大致投入 |
|---|---|---|---|---|
| 会用 | 提示词写得稳 | 无 | 个人提示词模板库 | 3~7 天 |
| 会接 | 代码里调得通 | 基础 Python 语法 | 自动化小脚本 | 1~2 周 |
| 会调 | 私有知识答得准 | 向量、检索基本概念 | 知识库问答 Demo | 1~2 个月 |
千万别跳级。我见过不少人第一段还没走完,就去折腾本地部署和微调,显卡驱动装了两天,最后连一句像样的提示词都写不出来,直接放弃。顺序错了,成本会翻十倍。
2. 第一阶段:把模型真正用起来
2.1 三条上手路线,按你的条件选
想用上大模型,其实有三条路,成本从零到几千块不等,别一上来就选最贵的那条。
第一条路最省事:直接用网页版或客户端。零成本、零配置,适合所有人打基础。缺点是没法自动化,也没法接你自己的数据。第一阶段的练习,这条路完全够用,别瞧不起它。
第二条路是用接口调用。按量付费,几分钱到几毛钱一次,适合想做自动化、想接自己数据的人。开个账号、拿个密钥、写二十行代码就能跑通,这是从“会用”跨到“会接”的关键一步。
第三条路是本地部署开源模型。需要显卡,好处是数据不出本机、可以随便折腾、调用不要钱。这条路适合有硬件条件、或者对数据隐私有硬要求的人。但我要说句实话:如果你是零基础,本地部署应该是最后才碰的东西,不是第一课。很多人被“私有部署”这四个字吸引,第一周就陷在环境依赖里出不来。
如果你确实想走本地路线,先算显存,别等下载完 14G 的权重才发现跑不动。粗略估算公式是:
显存需求 ≈ 参数量 × 每个参数的字节数 × 1.2(额外开销系数)按这个算:一个 7B 模型,用半精度(每个参数 2 字节)需要 7 × 2 × 1.2 ≈ 16.8 GB;用 8 位量化(1 字节)约 8.4 GB;用 4 位量化(0.5 字节)约 4.2 GB。13B 模型对应就是 31 GB、15.6 GB、7.8 GB。所以一张 8G 显存的卡,跑 7B 的 4 位量化版本刚好够,跑 13B 就得上 16G。这个账必须先算,不然就是白下载。
注意:量化会掉精度,4 位量化在通用对话上感觉不明显,但在代码生成和数学推理上掉得厉害。如果你主要用这两类任务,宁可换小一号的模型也别硬上 4 位。
2.2 提示词不是“跟它聊天”,是“给它写需求文档”
这是我见过的最大的认知偏差。绝大多数人写提示词的方式是“帮我写个总结”,然后抱怨效果不好。你换位想想,如果你把这句话甩给一个新来的实习生,他能交出你想要的东西吗?他只会交出一份他觉得对的总结。
好的提示词有四个零件,缺一个效果就打折。角色——你是什么身份;任务——具体要做什么;约束——不能做什么、边界在哪;输出格式——结果长什么样。四个零件凑齐,输出质量立刻上一个档次。
对比一下就很清楚。差的做法是:
帮我总结一下这篇文章好的做法是:
你是一位有十年经验的行业分析师。 任务:阅读下面的文章,提炼核心观点。 约束: 1. 只基于文章内容,不要补充外部信息; 2. 如果文章没有明确结论,直接说“原文未给出结论”,不要推测; 3. 保留原文中的具体数字和专有名词。 输出格式: - 一句话主旨(不超过 40 字) - 3 到 5 条要点,每条不超过 30 字 - 一段 100 字左右的延伸说明 文章内容: """ (粘贴正文) """差别在哪?后者把“我心里想要什么”翻译成了“机器能执行的规则”。特别是那句“如果没给出结论就说没有”,这一条能挡掉八成的幻觉。
再说一个被严重低估的技巧:用分隔符把指令和素材隔开。上面例子里的三个引号就是干这个的。你把一大段待处理文本直接跟在指令后面,模型经常分不清哪句是指令、哪句是素材,尤其当素材里本身就有“请总结”这类字样时,它会真的去执行素材里的指令。用"""、---、###这类明显分隔符包起来,问题基本消失。
还有一个提升准确率的笨办法:给例子。你希望它输出什么格式,就先手动写两个“输入→输出”的示范塞进去。这招在分类、抽取、格式转换类任务上效果立竿见影,比写一百字规则描述都管用。代价是提示词变长、消耗的 token 变多,但对准确性要求高的场景,这笔账很划算。
最后一个技巧叫“让它先想再答”。对于需要推理的任务,在提示词末尾加一句“请先分步骤说明你的分析过程,最后再给出结论”,准确率往往能明显提升。原理很简单——它把中间步骤写出来了,后面的结论就有依据可循,而不是一步跳到答案。不过这招也有副作用,输出会变长,而且中间步骤如果错了,会错得更自信,所以对关键结论还是要人工核验。
2.3 那几个参数到底调什么,一次说清
界面上那些滑块不是摆设,但也不用全调。日常只需要盯住三个。
温度(temperature)控制“随机性”。数值越低,输出越保守、越可复现;越高越发散、越有创意。我的经验值:做数据抽取、格式转换、代码生成,压到 0 到 0.3;日常问答、写文档,0.6 到 0.8;写文案、起标题、头脑风暴,可以到 1.0 以上。
top_p控制“候选池大小”,和温度的作用有重叠。这两个参数不要同时调,容易互相打架,调起来也没有方向感。业内的习惯是固定其中一个,调另一个。我一般固定 top_p 为 1,只调温度,好理解也好复现。
最大输出长度(max_tokens)控制回答能有多长。这个必须设,不设的话模型可能啰嗦到把额度耗光。反过来,设太小会导致回答被硬生生截断在半句话上。经验做法是按你期望长度的 1.5 倍设。
| 参数 | 作用 | 推荐值(抽取/代码) | 推荐值(通用问答) | 推荐值(创意) |
|---|---|---|---|---|
| temperature | 随机性 | 0 ~ 0.3 | 0.6 ~ 0.8 | 1.0 ~ 1.2 |
| top_p | 候选池 | 1(不动) | 1(不动) | 1(不动) |
| max_tokens | 输出上限 | 期望长度 ×1.5 | 期望长度 ×1.5 | 期望长度 ×1.5 |
| 重复惩罚 | 抑制复读 | 1.0 ~ 1.1 | 1.0 ~ 1.1 | 1.05 左右 |
心得:调试阶段把温度设成 0,这样同样的输入每次输出基本一致,改提示词的时候你能判断出“效果变好是因为我改对了”,而不是“这次碰巧抽到了好结果”。等提示词稳定了,再把温度调回正常值。
3. 第二阶段:让模型接进你的流程里
3.1 一次接口调用的最小结构
从“网页聊天”跨到“代码调用”,核心就一件事:你要按固定格式组织一段 JSON 发过去,再解析回来的 JSON。所有平台的接口在这个层面都大同小异,学会一个,其余半天就能迁移。
最关键的认知是messages这个数组。它不是一段纯文本,而是一个有角色标记的对话历史。三种角色:system定基调(你是谁、有什么规矩),user是用户说的话,assistant是模型之前说过的话。把 system 用好了,等于给整个会话装了一个常驻的系统提示词,比每次在用户消息里重复交代要省事得多,也更不容易被后续对话冲淡。
下面是一个最简的调用骨架,用通用 HTTP 请求写,你可以照着换成任意平台的地址和密钥:
import json import requests API_URL = "https://你的服务地址/v1/chat/completions" API_KEY = "你的密钥" def chat(user_text, system_text="你是一个严谨的助手。"): payload = { "model": "your-model-name", "messages": [ {"role": "system", "content": system_text}, {"role": "user", "content": user_text} ], "temperature": 0.3, "max_tokens": 1024 } headers = { "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } resp = requests.post(API_URL, headers=headers, data=json.dumps(payload), timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] print(chat("用三句话解释什么是向量检索"))这段代码里有几个地方看着不起眼,但都是实战踩出来的。timeout=60必须加,模型响应慢的时候不加超时会一直挂着。raise_for_status()必须加,不然接口报错你还以为是内容为空。密钥千万别硬编码在代码里然后传到公开仓库,用环境变量读,这是最基础的安全习惯。
多轮对话怎么实现?很多人以为接口有“会话”概念,其实没有。你要自己维护一个列表,每次把历史消息全带上,接口是无状态的。所以轮次一多,token 消耗会飞快地涨,因为每轮都要重发全部历史。这也是为什么长对话到最后模型会“忘事”——不是它忘了,是你带的历史被截断了。
流式输出是另一个必学项。默认模式下模型把整段话生成完才一次性返回,用户要干等好几秒。流式模式下文字一个一个吐出来,体验完全不同,尤其在做交互式应用时几乎是必需的。实现上就是把请求参数里加一个流式开关,然后按行读取返回的内容,逐块拼接。
3.2 让输出变成程序能用的数据
模型返回的是自然语言,程序要的是结构化数据,这中间有个鸿沟。最常见的做法是要求它输出 JSON,但光要求不够,一定要校验。我早期的脚本就是直接json.loads模型返回的内容,十次里有两三次崩在解析上——因为它有时候会在 JSON 前面加一句“好的,以下是结果:”,有时候用中文引号,有时候少个逗号。
稳妥的做法是三层防护。第一层,提示词里明确写“只输出 JSON,不要任何解释文字、不要代码块标记”,并给一个完整的示例。第二层,解析前先做一次清洗,把可能混进来的说明文字和代码块符号剥掉,只截取第一个左花括号到最后一个右花括号之间的内容。第三层,解析失败就重试,最多两次,重试时把错误信息一起塞回去告诉它“上次的输出无法解析,请严格按格式重来”。
对于更复杂的场景,现在很多平台支持“结构化输出”或“工具调用”能力,你可以直接传一个字段定义过去,由服务端保证格式合法。能用这个就用这个,比自己在提示词里死磕 JSON 靠谱得多。
注意:即使格式完全合法,也要校验内容。我遇到过它把“金额”字段填成字符串“未知”、把日期填成“下周一”的情况。格式对了不等于语义对了,业务层的校验一个都不能省。
3.3 Token 和成本,这笔账必须自己会算
新手最容易忽略的就是成本。它不像服务器按月付费,是用多少花多少,不知不觉就超了。
先说 token 是什么。它不是字,也不是词,是模型自己的一套“切分单位”。经验值:英文大约 1 个 token 对应 0.75 个单词;中文比较复杂,一个汉字大约在 0.6 到 1 个 token 之间,具体看分词器。做预算时按“1 个汉字 ≈ 1 个 token”粗算,偏保守但不容易出错。
一次调用的费用 = 输入 token 数 × 输入单价 + 输出 token 数 × 输出单价。注意输出通常比输入贵好几倍,所以“让它少说废话”不只是体验问题,直接关系到账单。
举个实际场景:你要批量处理 5000 份文档,每份平均 2000 字,让模型输出 200 字摘要。输入就是 5000 × 2000 = 1000 万 token,输出 5000 × 200 = 100 万 token。如果输入单价是每百万 token 若干元、输出单价是输入的 3 倍,这一趟下来的费用可能是几百块。看着不多,但如果你的提示词写得很啰嗦,每次都塞三千字规则进去,成本立刻翻倍。
省钱的路子有三条,按性价比排序。第一,精简提示词,把能删的客套话删掉,把示例从五个减到两个。第二,分级处理,简单任务用便宜的小模型,复杂任务才用大模型,先让小模型跑一遍,拿不准的再升级。第三,利用缓存,如果你的提示词前缀固定,很多平台对重复的前缀有折扣,把不变的部分放最前面、变化的部分放最后,能省不少。
还有一个坑要提醒:上下文窗口大不等于你能随便塞满。窗口是上限,不是推荐值。塞得越满,模型对中间部分的注意力越弱,业内管这叫“中间遗忘”,重要信息尽量放在开头或结尾。我做过对比测试,同样的问题,把关键资料放在开头和放在中间,准确率能差出可观的一截。
4. 第三阶段:让它回答你自己的知识
4.1 RAG 的整条链路,拆开看就七步
模型不知道你公司的事,这是硬伤。解决办法不是重新训练它,而是每次提问时把相关资料一起递过去,让它“看着材料回答”。这套做法叫检索增强生成,缩写 RAG。名字唬人,逻辑特别朴素——开卷考试。考试前你不需要把整本书背下来,只要知道去哪一页找答案就行。
完整链路有七步,每一步都能出问题:
第一步,文档解析。把 PDF、Word、网页、表格里的文字抽出来。这一步的坑最多,扫描版 PDF 抽出来全是乱码,双栏排版会被读成一行行错乱的字,表格更是重灾区。
第二步,切分。把长文档剁成小块,因为模型一次吃不下整本书,而且检索时你只需要相关的那几段。切多大、怎么切,后面细说。
第三步,向量化。用嵌入模型把每个文本块变成一串数字(向量),语义相近的块,向量距离也近。
第四步,入库。把这些向量存进专门的索引结构,支持快速查找。
第五步,检索。用户提问时,把问题也变成向量,去库里找最接近的几个块。
第六步,重排。初筛出来的结果按相关度重新排序,把真正有用的顶到前面。
第七步,拼接生成。把选中的资料塞进提示词,加上“只根据以下资料回答”的约束,交给模型生成。
4.2 切分和检索的参数,怎么定才不玄学
切块大小是最影响效果的一个参数。切太小,一句话被拦腰截断,语义不完整,检索出来全是碎片;切太大,一个块里混着好几个主题,检索时相关性被稀释,还白占上下文。
我的经验区间是中文 300 到 800 字。技术文档、条款类可以偏小,因为信息密度高、需要精确定位;叙述性长文可以偏大,因为上下文连贯性更重要。重叠部分设 10% 到 15%,也就是切 500 字的块,相邻块重叠 50 到 75 字。重叠是为了防止一句话正好卡在切口上,代价是存储和检索成本略增。
更重要的是按结构切,而不是按字数硬切。优先按标题层级、段落、章节切,让每个块自成一个语义单元。硬按字数切是最省事的做法,也是最容易出问题的做法。还有个小技巧:给每个块加上它的“出身信息”,比如“来自《XX操作手册》第 3 章第 2 节”,拼在块的正文前面。这样检索时标题里的关键词也能被匹配到,效果提升很明显,成本几乎为零。
检索返回几条(top_k)?3 到 8 条起步。太少容易漏,太多会引入噪声,还挤占上下文。有个常见误区是觉得“多给几条总没坏处”,实际上恰恰相反,无关资料会干扰模型的判断,它可能抓着一段不相关的内容强行编出答案。
只用向量检索够不够?不够。向量检索擅长“意思相近但说法不同”的情况,但对精确匹配很弱——比如查一个型号、一个人名、一个编号,关键词检索(传统倒排索引)反而更准。所以生产环境基本都是混合检索:向量召一批,关键词召一批,合并去重后再重排。这一套组合拳打下来,召回率提升非常明显。
| 参数 | 作用 | 起步值 | 调整方向 |
|---|---|---|---|
| 切块大小 | 每块的文字量 | 500 字 | 答案碎片化就调大,噪声多就调小 |
| 重叠比例 | 相邻块重复量 | 15% | 切口丢内容就调大 |
| top_k | 召回条数 | 5 | 漏答就加,答非所问就减 |
| 相似度阈值 | 过滤低分结果 | 0.3 左右 | 按实际分布测,别照抄 |
| 重排数量 | 精排候选数 | 20 进 5 | 按算力预算定 |
4.3 评估怎么做,别凭感觉说“好像还行”
RAG 系统最怕的就是“感觉还不错”。你随便问几个问题它答对了,上线之后用户问的稍微偏一点,就开始胡说。必须有一套评估办法。
最朴素也最有效的办法是攒一套测试集。从真实使用场景里挑 30 到 50 个问题,人工把正确答案和“应该引用哪几段资料”标注出来。然后每次改动参数,都跑一遍这套题,看两个指标:召回率(该找到的资料有没有找到)和忠实度(答案是不是严格来自资料,没有自己编)。
分开看这两个指标特别重要,因为它们定位的是不同的问题。召回率低,说明检索环节有问题——可能是切分不对,可能是嵌入模型不适合你的领域,可能是 top_k 太小。忠实度高但召回率低,会出现“它很诚实地告诉你资料里没有”,用户觉得它没用。反过来召回率高但忠实度低,就是“资料明明给了它,它还是自己编”,问题出在生成环节——提示词约束不够,或者资料被塞在中间被忽略了。
常见的失效场景我整理成了一张表,遇到问题先对照着查:
| 现象 | 最可能的原因 | 优先排查动作 |
|---|---|---|
| 答“资料中未提及”,但文档里明明有 | 切分把答案切碎了 / 召回没命中 | 打印召回结果,看原文块是否完整 |
| 答案对但编造了细节 | 提示词约束太弱 / 温度太高 | 加“只用原文表述”约束,温度降到 0.2 |
| 表格数据答错 | PDF 解析把表格拍平了 | 单独处理表格,转成 Markdown 再入库 |
| 问型号、编号查不到 | 纯向量检索对精确词不敏感 | 加关键词检索,做混合召回 |
| 同一问题两次答案不同 | 温度高 + 无重排 | 温度归零,加固定重排策略 |
| 长文档后半部分总答错 | 资料被塞在上下文中间 | 把最相关资料放最前面,减少条数 |
5. 动手搭一个最小可用的知识问答 Demo
5.1 目录结构和依赖,先摆清楚
光看原理不动手,等于没学。这一节带你搭一个能跑起来的最小版本,不用花哨框架,把链路走通最重要。
先说目录结构,简单到不能再简单:
qa_demo/ ├── docs/ # 放原始文档 ├── index/ # 向量索引持久化目录 ├── build_index.py # 建索引脚本 ├── ask.py # 提问脚本 └── config.py # 配置集中放这里config.py里放四样东西:文档目录、索引目录、嵌入模型标识、对话模型标识。集中管理的好处是换模型只改一个地方,不用满项目找。
依赖这块,最小集合是三样:一个文档解析库(处理 PDF 和 Word)、一个嵌入模型(本地跑或调接口都行)、一个向量索引库(支持持久化和相似度检索)。如果不想装本地嵌入模型,直接调接口把文本转成向量也可以,省显卡但要走网络。
提示:第一次搭建议全部用接口版,本地一个模型都不装。先把链路跑通,再考虑把哪一环换成本地部署。顺序反了,你会在环境问题上耗掉大半热情。
5.2 建索引这一步,细节全在解析上
建索引的流程是:遍历文档 → 抽文字 → 切块 → 向量化 → 存库。看着是五行代码的事,真正决定成败的是抽文字那一步。
PDF 解析要格外小心。文本型 PDF 直接抽一般没问题,但双栏排版的会串行,页眉页脚会混进正文,页码会变成噪声。我的处理办法是:抽完之后先做一轮清洗,把纯数字行、重复出现的页眉页脚、多余空行过滤掉。这一步不做,你会发现在检索结果里总能召回到一堆页码和公司名。
表格单独处理。表格抽成文字后往往变成一堆没有对应关系的数字,模型根本看不懂哪一列对应哪个表头。稳妥做法是把表格转成 Markdown 格式,表头和数据行一一对应,再作为一个独立的块入库。多花十分钟,问答准确率提升一大截。
切块函数的核心逻辑大概是这样:
def split_text(text, chunk_size=500, overlap=75): """按段落优先切分,段落太长再按字数硬切""" paragraphs = [p.strip() for p in text.split("\n") if p.strip()] chunks, buf = [], "" for p in paragraphs: if len(buf) + len(p) <= chunk_size: buf += p + "\n" else: if buf: chunks.append(buf.strip()) # 保留尾部重叠,防止语义被切断 buf = buf[-overlap:] + p + "\n" if len(buf) > overlap else p + "\n" if buf.strip(): chunks.append(buf.strip()) return chunks def enrich(chunk, source, section): """给每个块加上出身信息,提升检索命中率""" return f"【来源】{source} 【章节】{section}\n{chunk}"enrich这个函数看着可有可无,实际效果非常明显。用户问“报销标准是多少”,块正文里可能只写了“每月不超过 800 元”,没有“报销”两个字;但加上章节名“第三章 差旅报销标准”之后,这段文字就和问题对上了。
5.3 提问环节,提示词是最后一道闸门
提问流程是:问题向量化 → 检索 top_k → 拼提示词 → 调模型 → 返回答案和引用来源。
提示词这一环决定了模型会不会老实。我用的模板大概是这个意思:
你是资料检索助手,只能依据下面的【参考资料】回答问题。 规则: 1. 答案必须能在参考资料中找到依据,禁止使用你自己的知识补充; 2. 如果参考资料不足以回答,直接输出「根据现有资料无法回答」,不要猜测; 3. 回答末尾用【来源:文件名 - 章节名】标注依据; 4. 涉及数字、日期、专有名词时,必须与原文完全一致。 【参考资料】 {context} 【问题】 {question}第 3 条要求标注来源,这个设计有双重好处:用户能核实,你自己调试时也能一眼看出它引用的是不是正确的那一段。第 4 条专门对付数字被改写的问题——不加这条,它经常把“800 元”写成“约 800 元”,把“3 个工作日”写成“三天左右”。
调试阶段我建议在脚本里把召回的原文块打印出来。很多时候你以为模型在胡说,其实是检索环节就没找到正确内容,模型手里根本没有答案,只能硬编。先看召回,再看生成,这个排查顺序能省下大量时间。
实测下来,这套最简方案在几百份文档、几千个块的量级上完全够用,响应时间也在可接受范围内。真正上规模之后才需要考虑重排模型、分片索引、并发优化这些,那是后话。
6. 微调这事,先想清楚再动手
6.1 提示词、RAG、微调,到底该选哪个
一说“让模型更懂我的业务”,很多人的第一反应是微调。这是个非常常见的误区,而且代价不低——要准备数据、要显卡、要调参、要评估,一套下来没个把月下不来,效果还未必比改改提示词好。
我把三者的适用场景理了一下,选型时照着对号入座就行:
| 手段 | 解决什么问题 | 成本 | 见效速度 | 什么时候别用 |
|---|---|---|---|---|
| 提示词优化 | 输出格式不对、语气不对、偶尔跑偏 | 极低 | 分钟级 | 需要模型掌握它没见过的事实 |
| 检索增强 RAG | 需要私有知识、需要可追溯来源、知识经常更新 | 中等 | 天级 | 需要改变输出风格和固定格式 |
| 微调 | 需要固定的输出风格、特定领域表达习惯、稳定格式 | 高 | 周级到月级 | 知识类需求,或者数据量不足 |
一句话总结:知识用检索,风格用微调,格式用提示词。你公司今年的新政策,微调一百遍也没用,因为那是“事实”,不是“能力”;但你要是希望它输出的话术永远符合你们客服的腔调,那微调就很合适。
还有一个判断标准是数据量。微调想有效果,高质量样本至少几百条,稳妥起见上千条。如果你手上只有几十条,别折腾了,老老实实写提示词加示例,效果不会差。
6.2 LoRA 微调,为什么它能把门槛降下来
真要做微调,现在基本都用 LoRA 这类低秩适配方法,而不是全参数微调。原因特别实际:全参微调一个 7B 模型,光是优化器状态和梯度就要吃掉几十上百 G 显存,普通人根本玩不起;LoRA 只训练一小部分附加参数,显存需求能降到原来的零头,单张消费级显卡就能跑。
原理用大白话讲:原来的模型权重全部冻住不动,在旁边挂两个很小的矩阵,让它们相乘去“模拟”出需要调整的那部分变化。因为这两个矩阵很小,参数量只有原来的千分之几,所以训练快、存得也小——训完可能就是一个几十兆的文件,而不是十几 G。用的时候把这个小文件“挂”到原模型上就行,原模型本体一个字节都不用改。
这个设计带来一个额外好处:你可以给同一个基座模型挂不同的适配文件,切换不同任务,相当于一个底座配多套技能包,存储成本极低。
实操层面,真正花时间的不是训练,是数据准备。样本格式通常是“指令 + 输入 + 期望输出”的三元组,或者一问一答的对话格式。这里有个铁律:数据质量远比数量重要。一千条精心清洗的样本,效果远好于一万条从网上随便爬来的脏数据。我自己踩过这个坑,早期图省事用了一批自动生成的样本,训出来的模型学会了各种奇怪的格式和口头禅,比不训还糟。
6.3 训练之外,那些没人告诉你的坑
第一个坑是灾难性遗忘。模型在你的专业数据上训久了,通用能力会退化。表现是它在你的领域里答得特别溜,但你问它一个常识问题,它开始胡言乱语。缓解办法是往训练数据里掺入一部分通用样本,一般占 10% 到 20%,让它在学新东西的同时不把旧东西忘光。
第二个坑是过拟合。训练损失一路下降看着很爽,但模型开始逐字背诵训练集里的话,换个问法就不会答了。对策是留一份独立的验证集,只要验证集上的表现开始变差就停,不要死盯着训练损失。
第三个坑是评估缺失。很多人训完就随手问两个问题,觉得“挺像那么回事”就上线了。这跟没评估一样。必须准备一套固定的测试题,训之前跑一遍、训之后跑一遍,用同一套题对比,才能知道到底有没有变好。
第四个坑是学会了坏习惯。数据里如果有格式错误、错别字、混乱的标点,模型会照单全收。清洗数据这一步必须人工抽样看几十条,机器过滤只能拦住明显的脏数据,细微的格式问题还得靠眼睛。
| 坑 | 典型表现 | 对策 |
|---|---|---|
| 灾难性遗忘 | 通用问题答得越来越差 | 混入 10%~20% 通用样本 |
| 过拟合 | 换个问法就懵 | 留验证集,早停,别贪轮次 |
| 数据脏 | 输出带奇怪格式和口头禅 | 人工抽样检查,严格清洗 |
| 无评估 | 说不出到底好了没有 | 固定测试集,训前训后对比 |
7. 踩过的坑和排查手册
7.1 高频问题速查
入门到进阶这一路,遇到的问题其实高度重复。我把最常被问到的整理成一张表,遇到状况先对照着查,能省下大量瞎试的时间。
| 问题现象 | 大概率原因 | 怎么解 |
|---|---|---|
| 回答总在半句话被截断 | 最大输出长度设小了 | 调到期望长度的 1.5 倍 |
| 让它输出 JSON 总解析失败 | 模型爱加解释文字 | 清洗 + 强约束 + 失败重试两次 |
| 长对话后模型“失忆” | 历史超窗口被截断 | 做历史摘要,或只保留最近几轮 |
| 同样输入结果每次差很多 | 温度偏高 | 调试期温度归零 |
| 中文输出夹生英文 | 提示词没约束语言 | 显式写“全程使用中文” |
| 答案很啰嗦,全是废话 | 没有格式和长度约束 | 明确“不超过 N 字”“用列表” |
| 接口偶发超时 | 没设超时或网络抖动 | 加超时 + 指数退避重试 |
| 账单突然飙升 | 提示词过长 / 历史未裁剪 | 精简前缀,限制历史轮数 |
7.2 几条个人经验,都是真金白银换的
第一条,先把固定流程跑通,再谈优化。我见过太多人一上手就研究重排模型选型、嵌入模型对比,主线流程一次都没跑通过。先让它能跑,再让它跑好,这个顺序不能反。
第二条,保留可观测的中间结果。你的脚本里一定要能打印出“这次召回了哪几段、拼出来的提示词长什么样、原始返回是什么”。这三样东西在出问题时就是救命的,没有它们你只能靠猜。
第三条,别迷信评测榜单。榜单上的分数和你实际场景的表现经常对不上,尤其是中文专业领域。别人说好用的模型,到你这可能就不行。唯一的办法是在你自己的测试集上跑。
第四条,把提示词当代码管起来。用版本管理的思路对待它,每次改动留下记录,标注改了什么、为什么改、效果变化如何。调提示词是个反复试错的过程,没有记录你会陷入“改了三天,忘了最初那个版本为什么更好”的困境。
第五条,对输出的信任要有分层。润色、头脑风暴这类任务,输出直接可用;摘要、抽取类任务,抽检即可;涉及数字、结论、对外发布的内容,必须逐条核对。我自己的底线是:凡是进了正式文档的,一个字都不放过。
这套东西我断断续续调了一年多,最大的体会是——大模型本身没那么玄,难的是把它塞进一个要求精确的流程里,还得让人放心。工具会一直换,模型的代号一年能变好几茬,但你搭起来的那套“验证—观测—迭代”的习惯不会过期。真正拉开差距的从来不是你用了哪个模型,而是你知不知道该在什么时候不信任它。