这篇笔记是系列的第二篇。第一篇讲的是从零开始感受AI能力边界,聊了聊大模型能做什么、不能做什么。这篇换个角度,直接从“会聊天”走向“能干活”,把提示词工程、上下文管理、检索增强生成(RAG)、Agent雏形这些概念串起来,用一套完整的实战案例告诉大家,怎么把一个只会对话的模型,变成能帮你处理实际任务的工具。
如果你是那种已经用完各类AI助手、觉得“AI不过如此”的朋友,这篇笔记可能是你需要的转折点。因为我会直接给你一套可以复现的工程思路,让你看到所谓“进阶用法”到底在解决什么问题——本质上就是在管理模型的上下文、约束模型的输出、给模型外挂知识库、最后让它自己决定调用什么工具。
1. 先从一次真实的“翻车”聊起:为什么AI总在关键时候掉链子
前一阵我帮一位做运营的朋友搭了一个“产品文案助手”,需求非常朴素:输入产品名称和卖点,AI自动生成三版不同风格的朋友圈文案。第一天跑通的时候效果惊艳,输出质量吊打市面上多数模板工具。结果第二天一上班,朋友发来消息:“它抽风了,写的文案驴唇不对马嘴。”
我跑过去看了一眼,发现问题出在一个特别不起眼的地方:朋友在对话历史里粘贴了一张产品截图,然后继续追问新产品的文案需求。模型看得到前面的截图,就会同时参考截图中的旧产品和命令里的新产品,两个信息源相互冲突,输出自然飘了。
这个场景听起来很初级,但它背后藏着一个核心概念——模型本身没有记忆。它每一次回答都在读取完整的上下文,然后根据这些文字预测下一个最可能的词。你以为它在和人聊天,其实它在做一场高难度的“接龙”游戏。你把什么喂进上下文,它就基于什么来写下文。
这也是为什么AI进阶的第一课,不是学什么花哨算法,而是学会管理上下文。
很多人用不好AI,根本原因就是不会“喂”信息。要么一股脑把几万字资料塞进去,要么挤牙膏一样一句一句问,模型的输出质量当然不稳定。我打个比方:你把一个新人员工叫进会议室,他能不能给出靠谱的答案,取决于你给的背景资料是否完整、问题是否明确、期望的输出格式是否清晰。模型也一样。
因此,这次笔记我搭了一个小项目,名字叫“某跨平台知识问答系统”,用来验证一套可复现的AI应用构建流程。整体思路并不复杂,拆开看就四层:
- 上下文构造层:决定喂给模型什么内容,以什么顺序、什么格式喂。
- 指令约束层:定义模型的角色、任务、输出格式和禁忌事项。
- 知识增强层:让模型在回答时先检索你的私有资料,而不是凭空编造。
- 工具调用层:让模型在需要的时候主动去查数据、算逻辑,而不是硬着头皮“编”。
下面每一层都是这次笔记的重头戏。我用实际代码和踩坑记录来讲,尽量做到你能直接复制去用。
2. 上下文构造:先学会和模型“对齐”
2.1 为什么上下文结构比模型参数还重要
有个说法我很认同:同样的模型,会喂上下文的人和不会喂的人,用出来的效果差距可能比换一个更大参数的模型还明显。这不是玄学,是有工程依据的。
主流大模型大多采用Transformer架构,核心机制是自注意力。模型在生成每一个词的时候,都会回看整个输入序列,计算输入中哪些词和当前词最相关。你的上下文越长,模型需要关注的潜在相关点就越多,注意力被稀释的概率就越大。所以在构造上下文时,要把握一个原则:信息密度优先,长度其次。
我自己在项目里验证过,同样是问“帮我总结这份季度报告”,以下两种上下文的输出质量差距巨大。
第一版(信息低密度):
用户:这是一份季度报告,帮我总结一下。 系统:请总结以下季度报告:{整篇报告复制粘贴进来,8000字}第二版(信息高密度):
系统:你是一名资深商业分析师,擅长从报告中提炼关键结论。请阅读报告内容,并以“重要结论、关键数据、风险提示”三部分输出。报告正文如下。 用户:{只粘贴报告中包含核心结论、关键数据的片段,约1500字}第二版的输出稳定很多,因为系统消息把模型的“角色”锁定在了分析师位置,用户消息里又做了一个粗糙的降噪,只留下最有价值的信息。模型不需要在一堆口水话里大海捞针,自然更容易输出高质量结果。
2.2 一套实用的上下文组织模板
在“某跨平台知识问答系统”里,我用了一套固定模板,这里分享出来。如果只记住这一条笔记,建议记住这个。
def build_context(question, retrieved_chunks, history): context = { "system": ( "你是一个严谨的智能助手。请严格依据参考资料回答用户问题。" "如果参考资料中没有相关信息,请明确回答'资料库中未找到相关内容'" "不要编造事实。回答控制在300字以内,使用简洁的中文。" ), "history": history[-4:], "user": { "question": question, "reference": "\n\n".join(retrieved_chunks), "format_instruction": ( "请按以下结构输出:\n1. 直接答案\n2. 依据说明\n" ) } } return context这个模板做了几件事:
- 给模型框定了角色和边界:你是“严谨的智能助手”,意味着不太需要额外解释。
- 限制输出长度:300字以内,避免模型放飞自我写小作文。
- 强制引用来源:让回答有迹可循,把凭空编造的概率降下来。
- 截断历史对话:只保留最近4条,防止过长的历史干扰模型对当前问题的判断。
第4条是我在项目里临时加的。一开始没做截断,跑了一周后发现一个典型问题:历史消息越翻越多,模型开始“过度参考”旧对话里偶尔出现的错误信息,甚至出现用户已经改口了新问题、模型还在延续旧话题的情况。加上截断后,这个问题基本消失。
2.3 关于上下文窗口的执念可以放下了
这一两年各家模型都在拼参数,动辄几百K的上下文窗口听起来很唬人。但我实际测试下来的感受是:上下文窗口大,不等于你该把它用满。就像一个大冰箱,你可以塞进一整头牛,但如果你只是想煎块牛排,没必要把冰箱塞满。
窗口越大,注意力分散的可能性越大,处理延迟也会变高,更重要的是,很多模型在中长文本上的表现存在“迷失在中间”的现象——开头和结尾的信息容易被模型关注,中间夹着的重要信息往往被忽略。因此我给自己定的策略是:能用精简摘要解决的,绝不塞原文;能用取片段解决的,绝不整个文件丢进去。
3. 提示词工程:从“会问问题”到“会下指令”
3.1 为什么同一件事,换个问法效果天差地别
我知道很多人都看过那种“神仙提示词”合集,用的人觉得玄,不用的人觉得扯。但提示词工程的存在是合理的,只是市面上很多传播版本把它包装得过于玄学。拆开来看,所有高级提示词的底层逻辑就那么几条。
先说最容易被忽略的一条:模型是基于概率生成内容的,你的指令越模糊,它的输出概率空间越大,结果就越不可控。你问“怎么写好一篇文章”,模型可以给你一万种答案;你问“请以产品运营的身份,为一场面向新用户的活动写三段不同风格的邀请文案,每段不超过50字”,模型能够输出的范围就被大大压缩了,质量自然提升。
那为什么很多人用了结构化提示词还是效果不好?因为大部分人只学会了“堆砌指令”,却没有理解指令之间的优先级。当你的提示词里同时出现“语言生动有趣”和“保持专业严谨”这类互相矛盾的指令时,模型只能随机选择一个方向执行,结果就是两种风格都不太像。
3.2 一个亲测有效的提示词四层结构
我在项目里把提示词拆成了四层,按优先级从高到低排列:
- 角色锚点:你是谁,你的立场是什么。
- 任务指令:你要做什么,不要做什么。
- 约束条件:输出格式、长度、语气、引用规则。
- 示例引导:很少量但高质量的示例,比大段文字描述有效得多。
这四层有一个明显的好处:每一层都在给模型“缩小搜索空间”。角色锚点决定口吻,任务指令圈定内容范围,约束条件卡死输出框架,示例引导给出参照系。层与层之间不冲突,模型执行起来负担也小。
来一个直观对比。同样是“让模型改写一段话”。
低质量提示词:
用户:帮我把这句话改得更吸引人。 原文:本产品是一款智能保温杯,具备长效保温功能。其实这个写法不算差,但模型输出的“吸引人”理解会非常随机。同一段话,有人改成夸张广告体,有人改成文艺抒情体,有人改成硬核参数体。
高质量提示词:
系统:你是一位擅长社交媒体文案的编辑,文风犀利,喜欢用具体细节代替抽象描述。 用户:请改写以下产品介绍,目标是吸引25-35岁通勤上班族。 要求: 1. 避免使用“智能”“长效”这类抽象词。 2. 用具体使用场景贴合目标人群。 3. 总字数不超过60字。 原文:本产品是一款智能保温杯,具备长效保温功能。后者的输出基本可以稳定落在“通勤路上想喝口热的”这类场景化表达上,不仅含混度低,而且读者代入感更强。
3.3 少样本示例:最被低估的提示词手段
文本描述再详细,都不如直接给模型看一段“正确输出”来得直观。我在项目里专门维护了一个examples.json文件,里面存了五六组“问题—标准输出”对,每次调用模型前,挑两组最相关的拼进提示词里。
示例数量不需要多,多了反而干扰模型对核心规则的抓取。我实测下来,两三组高质量示例的效果远胜于十组参差不齐的示例。因为示例的本质是在告诉模型“目标分布长什么样”,你给的示例之间如果风格相互冲突,模型会学到错误的规律。
另外注意一点:示例里的占位信息要足够通用,避免模型只学会模仿字面结构而不是理解语义。我之前写过一组示例,里面统一用了“某款产品”,结果模型在正式回答里好几次把用户的问题误解成“某款产品”指代的对象,折腾了半天才发现是示例惹的祸。
4. 知识增强:打造一个会查资料的AI(RAG实现)
4.1 为什么模型需要外挂知识库
大模型最容易被人诟病的一点,就是它天生自带“一本正经胡说八道”的属性。随便问一个它的训练数据里没有的冷门问题,它大概率会编一个听起来合理但经不起推敲的答案。这就是业内说的“幻觉”。
幻觉的根源在于:模型的目标不是“告诉你真相”,而是“生成看起来合理的文本”。如果你只有这一个模型、没有别的信息来源,那它只能靠自己的“大脑”硬编。所以你一旦需要AI回答私有文档、最新数据、垂直领域问题,就必须给它一个外挂的知识库,让它在回答前先查资料,而不是凭记忆直接作答。
这个方案有个专业名字:检索增强生成,简称RAG。它的核心流程可以用一个很朴素的话总结:先生成答案前,先把相关资料捞出来,喂给模型当参考,再让模型基于这些参考作答。
4.2 最小可用的RAG全流程(附代码)
我在“某跨平台知识问答系统”里实现了一个最小版本的RAG,用了不到150行Python代码。流程分四步:
- 第一步:加载文档,按固定长度切块。
- 第二步:把每块文本转成向量。
- 第三步:把用户问题转成向量,在库里做相似度检索。
- 第四步:把命中的文本块拼进上下文,交给模型生成最终回答。
切块是第一个要注意的细节。太长会包含太多噪声,检索不精准;太短会破坏语义完整性。我实验下来,按200到400个字符左右切块,配合20%左右的重叠,效果比较均衡。为什么要有重叠?因为一个完整语义单元可能恰好被切在边界上,重叠可以在一定程度上缓解这个问题。
向量化部分,我用的是一个开源嵌入模型。嵌入模型的作用,是把一段自然语言变成一组几百维的浮点数数组。这样“今天天气怎么样”和“今日气候如何”这两句话,在向量空间里就会靠得很近,即使字面表达完全不同。
检索部分用到了最简单的余弦相似度计算,把用户问题和所有切片向量逐一比较,取最相似的前3条作为候选。为什么是3条而不是10条?因为候选太多,拼接进上下文后会把模型的注意力撑散,生成质量反而下降。
import numpy as np from sklearn.metrics.pairwise import cosine_similarity def retrieve(question_embedding, chunk_embeddings, chunks, top_k=3): # 把二维数组转成统一形状,逐条计算余弦相似度 scores = cosine_similarity([question_embedding], chunk_embeddings)[0] top_indices = np.argsort(scores)[-top_k:][::-1] return [chunks[i] for i in top_indices], [scores[i] for i in top_indices]代码很简陋,但意思到位了。真实生产环境下有很多更花哨的检索策略,比如混合检索、粗排精排两阶段、查询改写等等。但对一个从零起步的工程项目来说,先让最小流程跑通,比一上来就堆高级算法重要得多。
4.3 RAG里最容易被忽略的三个坑
第一个坑:切块策略直接影响检索质量,而且没有万能参数。我一开始默认用了500字符切块,结果问答准确率只到70%左右。把切块长度改成动态切分,按照自然段落边界来分段之后,准确率明显上升。建议在切块上多花一点时间,这决定了RAG系统的上限。
第二个坑:检索回来的内容不一定和问题相关。相似度检索只能保证“看起来像”,不能保证“意思对”。所以我在拼接参考内容时,会顺手把每条切片的相似度得分也传给模型,让模型在低分切片面前保持警惕。指令里会加一句:“若参考资料与问题相关性不高,请说明依据来源。”
第三个坑:每条历史记录都必须干净。RAG最怕用户之前在对话里吐过一口“毒奶”——比如聊到一半把某个数字说错了,这个错数字混进上下文,后续所有的检索结果都会带着错误的引导。所以对话历史入库之前,要做一个简单的清洗和只保留“用户问题和最终回答”的预处理。
5. Agent雏形:让AI从“回答者”变成“执行者”
5.1 什么是Agent,以及它和普通对话的区别
如果说RAG解决的是“AI不懂的怎么办”,那Agent解决的是“AI不能做的怎么办”。大模型本身没有行为能力的,它只能吐文字。但文字也可以变成指令,模型通过输出一段特殊格式的内容,就能触发一个外部动作。比如模型输出“调用天气查询API,参数:北京”,程序解析到这行字后,自动去调用天气接口。这个东西就是Agent的雏形,你给模型装上了手和脚。
从工程实现上看,Agent并不神秘。核心就两步:
- 模型根据用户问题,决定需要调用哪个工具。
- 程序解析模型的“工具调用请求”,执行对应函数,把结果再喂回给模型。
这就像一个公司里的文员,领导让他订机票,他先写出一张审批单,交给行政去执行,执行完再拿着结果回来向领导汇报。
5.2 一个10分钟能跑通的工具调用Demo
在项目里,我实现了一个最简Agent,只有一个功能:在本地CSV文件里查指定商品的价格,并计算折扣价。这个功能不需要联网,也不需要额外API,只需要让模型学会输出一段结构化指令。
我让模型输出的不是自然语言,而是标准JSON:
{ "tool": "query_price", "params": { "product_name": "某型号保温杯" } }程序收到这段JSON后进行两步操作:第一步解析JSON,识别工具名;第二步调用对应的Python函数,得到查询结果。模型看到查询结果后,再生成最终回复。整个流程串起来之后,用户端的体验就是:我说“帮我查一下那个保温杯打折后的价格”,AI先查了库,再回答“原价159元,限时8折,折后127.2元”。
关键代码思路如下:
def run_agent(question): # 第一步:让模型决定调用哪个工具 decision = llm.chat( user=question, system="判断是否需要查询数据库。如果需要,输出包含tool和params的JSON;如果不需要,直接回答。" ) # 第二步:解析JSON并执行 if "tool" in decision: result = execute_tool(decision["tool"], decision["params"]) return llm.chat(user=question, system=f"工具返回结果如下,请基于结果回答:{result}") return decision粗看只有十几行代码,但已经足够跑通“模型决策—函数执行—结果回填”这一整条Agent链路。
5.3 Agent设计中最关键的系统消息写法
做Agent时,系统消息的重要性比普通问答场景高一个量级。因为你不仅要告诉模型“怎么回答问题”,还要告诉它“什么时候该调用工具”“什么时候不该调用工具”。
我给Agent写系统消息时,有一个固定的规则块:
你有以下工具可用: 1. query_price:查询商品价格,参数:product_name。 使用工具的唯一条件是:用户问题涉及价格、折扣、库存等数据库信息。 如果用户只是聊天、提问常识,请直接回答,不要调用工具。这里面有一个很关键的约束:“使用工具的唯一条件”。如果不加这句话,模型很可能在用户随便提一句“你好”的时候也去调用工具,因为模型的默认倾向是“尽量表现得有用”。你给了它工具,它就想用。
加了这条约束之后,问题基本解决。模型开始学会“区分意图”——哪些问题需要动工具,哪些问题动嘴就行。这其实是Agent产品化过程中一个非常基础但必要的设计决策。
5.4 Agent比普通问答多出的三个麻烦
第一个麻烦:错误传播被放大了。普通问答模型答错了,后果止于一段文本。Agent答错了,可能触发一次不该触发的工具调用,比如查了错误商品、扣了不该扣的库存。所以我在调用工具前加了一个确认逻辑:当工具参数置信度不高时,向用户输出一个确认问题“你是在问某型号保温杯的价格吗?”而不是直接执行。这个确认步骤拉低了自动化程度,但大幅提升了安全性。
第二个麻烦:工具名称和参数容易混淆。模型容易把相似工具搞混,特别是工具数量超过3个之后。我在项目里做了一次小实验,工具从1个增加到4个时,模型选错工具的比例明显上升。如果工具很多,要给每个工具写清晰使用说明,并在工具之间做明显的边界描述。
第三个麻烦:模型会试图“绕过”工具返回的结果去编造答案。比如工具返回“商品不存在”,模型为了不让用户失望,可能会自己编一个相近的价格出来。这种问题在测试阶段容易被忽略,因为在开发环境里你往往预先知道正确答案。所以Agent的系统消息里要专门加一句:“当工具返回结果为不存在时,必须如实告知,不得自行推测参数。”宁可多提示一点,也不能让模型用幻觉去填肚子。
6. 实战中的排查清单与优化记录
6.1 我遇到过的四个高频问题
这套系统跑了一个多月,我遇到过的坑不算少,挑几个最典型的记录在这里,给后来者排雷。
- 问题一:模型输出不是合法JSON。原因通常是约束不够强。解决办法是冻结解码参数,或加一步格式校验;更稳妥的做法是用正则从纯文本里抽离出JSON片段,而不是要求模型“只输出JSON”。
- 问题二:检索出来的相关内容看着相关,但关键实体对不上。比如用户问“A型号手机保护壳”,检索命中的却是“B型号手机保护壳”。原因是切块太短,实体信息不完整。把切块长度调到300并引入20%重叠后,明显改善。
- 问题三:长对话过程中,系统逐渐“忘掉”最初指令。这种现象跟上下文窗口扩大有关,开头角色指令被后续对话素材稀释了。我的处理办法是:把系统消息重发一次,并放在最新一轮用户问题之前。让关键指令保持在最近的位置,时效性优先。
- 问题四:成本随调用量线性上涨。排查之后发现很多工具调用是“冗余”的。模型在不需要调用工具的时候也调了。调整系统消息之后,工具调用数量下降近四成,费用也跟着下来了。
| 问题类型 | 表现 | 排查方向 | 解决参考 |
|---|---|---|---|
| JSON解析失败 | 工具调用无法执行 | 模型输出混入解释文字 | 用正则抽取或约束模型输出 |
| 检索不精准 | 答案答非所问 | 切块策略不合理 | 调整切块长度,加入重叠 |
| 指令漂移 | 模型忘记原始约束 | 系统指令被长对话稀释 | 关键指令位置前移并重发 |
| 冗余工具调用 | 成本上升 | 模型意图识别不准确 | 限定调用条件并加示例 |
6.2 两个我认为比较高级的调试技巧
第一,给每一步中间输出留一个调试开关。我在代码里加了一个DEBUG环境变量,打开之后,系统会在控制台打印每一个中间环节的结果:用户原始问题、检索到的文本块、打分、拼好的上下文、模型原始输出。排查问题的时候,这一步价值极高。很多人遇到的问题,其实直接在“中间输出”环节就能发现,根本不用拿到最终回答去猜。
第二,用一个固定“测试集”做回归验证。AI应用改了一次提示词,经常是这个问题修好了,另一些问题却变差了。所以我在项目里专门准备了一个包含30条不同难度问题的测试集,每次调整完就批量跑一遍,用输出对比来确认改动到底有没有改善整体效果。这个方法在普通开发里非常常规,但在AI原生应用开发里,很多人反而会偷懒跳过,我建议大家不要省这几步。
6.3 成本控制的基础经验
跑AI应用,很多人最大的心理落差不是效果不够好,而是账单涨太快。作为业余项目,我没法像大厂那样烧钱做实验,所以摸索出一套省钱姿势。
- 每次都带上结构化指令,宁可用更长的系统提示词,也别让模型用“试错式”的回答来理解你的需求。
- 检索召回数量不要贪多。默认3条即可,召回10条,效果不提升百分之一,成本却显著增加。
- 能本地跑的环节绝不调云端API。嵌入模型、切块、相似度检索,全部本地化处理,只有最后的“理解与生成”才花钱调大模型。
- 模型回答设置合理上限。输出端的token数量直接和成本挂钩,而且经验证明,多数任务100到300个输出token完全够用。
7. 后续还可以延伸哪些方向
这套系统做完之后,我琢磨过一阵还能往哪里走。这里列几个我觉得有价值的方向,后续可能单独开笔记聊。
一个是把检索范围从纯文本扩展到多模态,让图片、表格也参与检索。现在不少嵌入模型已经支持图文混合向量化,做企业知识库的话,这个扩展很有必要。
另一个是把单一Agent升级成多Agent协作。比如让一个“编排者”模型负责拆解任务,把“查价格”和“写推荐语”分别派给不同Agent去执行,最后汇总。这个模式在复杂任务上表现会更好,但调试复杂度也指数级上升,不太适合新手一上来就碰。
还有一个方向是把系统里的所有配置模板化,做成一个可配置的控制台。这样非技术同学也能通过改配置文件来调整AI助手的性格、知识库和工具列表。目前这套东西的边界其实已经很清晰了,真正把它做成一个能被别人使用的产品,还需要花不少力气打磨。
按照我个人的习惯,这类笔记向来是以“踩坑记录”收尾的。这篇的最后,我想分享一句这次实战里感触最深的话:AI应用开发的瓶颈往往不是模型能力,而是你对任务的理解深度。把一个问题拆得足够清晰,AI就能表现得足够聪明;如果你自己都搞不清楚想让模型做什么,模型大概率会给你一个花团锦簇的废话。后面我计划把多Agent协作和图文检索这两个方向继续啃一遍,如果跑出有价值的东西,再来更新第三篇。