从零实战 Agent 与 RAG:让大模型学会“办事”和“查资料” 很多人第一次接触 Agent 和 RAG第一反应是这些名词背后肯定藏着很深的算法得先把 Transformer、注意力机制这些啃完才敢动手。我在实际带新人的时候发现解释这两个东西最好的方式不是从论文开始而是先记住两句话Agent 是让大模型学会“办事”RAG 是让大模型学会“查资料”。只要抓住这个方向剩下的都是工程细节。这篇文章的目标读者就是标题里说的“小白程序员”。你可能只写过增删改查接口可能只听过提示工程但没系统学过机器学习都没关系。我会从大模型解决不了的几个问题讲起分别拆解 Agent 和 RAG 的核心流程再给你两份可以直接复制运行的极简代码。最后还会把我自己在实际项目中踩过的坑、排查过的诡异问题整理成速查表方便你收藏后照着用。1. 为什么必须学 Agent 和 RAG大模型没那么全能1.1 大模型天生不擅长什么很多人以为大模型只要参数够大、训练数据够多就什么都知道。实际使用下来你会发现它至少有四件事天生做不好不知道最新信息不知道你的私有数据不能替你操作系统也不能保证长任务中途不出错。打个比方。你去问一个刚入职的实习生“今天会议室有没有空”他如果没有看系统、没有打电话只能凭印象乱猜。大模型也是这样它的训练数据截止于某个时间点之后发生的事它完全不知道。哪怕你问的是今天天气它也会一本正经地编一个可能不存在的答案。训练语料里没有录入的公司制度、产品文档、客户资料对它来说同样是盲区。另外大模型本身只是跑在服务器上的一个程序它没有“手”不能直接修改数据库、不能发邮件、不能调用内部接口。你让它“把第 5 号工单状态改成处理中”它只能告诉你该怎么改而不是真正去改。至于长任务更明显前面刚算完三步后面可能就把自己的结论忘了逻辑断裂、重复动作都是常事。这四点就是 Agent 和 RAG 存在的根本原因模型没有的资料用 RAG 临时塞给它模型做不了的动作由 Agent 安排工具去执行。1.2 Agent把模型从“聊天机器人”变成“办事员”Agent 不是一个神秘的新模型它是一套让模型反复决策的程序。想象一下你让一个实习生去处理客户投诉他需要先查订单系统发现数据异常再问主管怎么办然后根据答复去更新状态。Agent 的逻辑完全一样只不过“实习生”换成大模型“查系统、问主管”换成外部工具调用。这套循环最关键的地方在于模型并不真的执行操作它只是“决定要调用哪个工具、参数是什么”。真正执行的是你写的函数。比如模型说“调用查询订单接口参数是 ORDER-2025-001”你的代码就去请求接口拿到结果后再把它还给模型。模型看到结果后再决定下一步是继续查库存还是直接生成最终回复。所以 Agent 至少包含四个部分工具定义、决策入口、循环控制、状态整理。工具定义告诉模型“你有什么本事”决策入口负责把用户问题交给模型循环控制决定“要不要继续调模型”状态整理则是把多轮工具结果压缩成模型能看懂的上下文。理解这四个部分比只跑通一个 Demo 重要得多。1.3 RAG让模型在回答前先“开卷”如果你只是想让模型回答私有知识问题不想花大价钱微调RAG 是目前性价比最高的方案。RAG 的全称是检索增强生成工作流其实很朴素用户提问后先从你自己的文档库里找出最相关的几段资料再把这几个片段连同问题一起交给模型让模型基于资料作答。这个思路好比考试时允许带资料。模型不用把整本手册背下来它只需要在拿到题目后翻到正确的那一页照着重点答题。好处也很直观知识更新只需要替换文档索引不用重新训练回答有据可查模型瞎编的概率大幅降低对计算资源的要求也远低于微调。我在自己做的问答 Demo 里最初只是把一份几十页的产品手册硬塞给模型结果上下文直接超长费用高、回答还乱。后来改成 RAG只检索出与问题相关的两三段模型输出立刻稳定调用成本也降了一个数量级。对小白来说RAG 是你上手的第一个必学技能。2. 先别急着写代码把这两个概念彻底搞清楚2.1 RAG 不是向量数据库而是一套流程市面上很多教程会让人去安装某个向量数据库跑一个搜索脚本就说“RAG 完成了”。这是一个非常典型的误解。向量数据库只是 RAG 流程中用来存向量、做相似度检索的仓库它既不是充分条件也不是 RAG 的全部。真正的 RAG 流程至少包含文档加载、文本切片、向量化、索引构建、查询改写、检索、重排、答案生成。每一步都有独立的设计空间。切片切得多大会影响检索召回准不准向量模型选哪一个会影响语义相似度判断检索到的片段怎么拼进提示词会影响最终答案质量。这些都是 RAG 的一部分。所以当你听到别人说“我们用向量数据库搞定了 RAG”不要觉得他专业。你可以追问一句检索前有没有做查询改写切片策略是什么Top-K 是多少回答有没有做事实校验这些问题一问出来对方就知道你不是完全零基础。对小白来说前期甚至不需要引入数据库用 Python 的列表加余弦相似度就能跑通全流程。2.2 Agent 不是简单的“工具调用”类似地Agent 被误读为“模型支持 Function Call”。从 API 层面看模型返回参数确实叫 tools 或 function_call但完整的 Agent 需要多轮迭代。模型第一次返回“调用查天气接口”你的程序执行后需要把结果再次返回给模型模型再生成“今天的天气是晴天”这个最终回答。如果只做一次调用其实只是单次 Tool Calling还算不上完整的 Agent。区分标准很简单有没有围绕目标进行“决策—执行—观察—再决策”的循环。比如让模型查一个城市的天气后再推荐穿搭它可能需要先调用天气工具看到结果后再生成建议如果气温低于十度它可能还想再查一下风力。这中间的任何一步都可能需要重新调用模型。没有循环就没有 Agent。另外想清楚一个问题Agent 的“智能”来自哪里来自模型的推理能力也来自你给的工具描述。描述清楚模型知道什么时候该用、参数怎么填描述模糊模型就像手里有工具但看不懂说明书的人会频繁乱调或者干脆不用。2.3 两者合体先查资料再办事还是边办事边查资料实际业务里Agent 和 RAG 经常不是独立工作的。最典型的一个结合场景是智能客服工单系统用户说“我的订单已经三天没发货帮我查一下物流并发起催单”。这时 Agent 需要拆解任务第一步调用订单查询工具拿到订单状态第二步发现状态异常触发 RAG 查询内部关于物流异常的处理规则第三步根据规则调用催单工具第四步汇总结果回复用户。在这个过程里RAG 变成了 Agent 工具箱里的一个特殊工具。模型先判断“需要对内部知识库进行检索”然后调用这个检索工具的接口返回相关片段再进入下一步。可以理解为RAG 解决“知识从哪里来”Agent 解决“任务怎么推进”。两者结合就是当下大多数企业级大模型应用的标准架构。你不需要一开始就掌握这种复杂组合但心里要有这个整体图景。这样你在做最初 Demo 的时候才不会被后续业务扩展打乱节奏。3. 手写第一个 Agent20 分钟跑通工具调用3.1 最小环境准备与模型访问封装开始写代码之前先确认你的 Python 版本在 3.9 以上并安装好相关依赖。不同模型提供方的接口差异不小但只要是兼容格式的 SDK用法基本一致。我这里用一个虚构的 llm_sdk 示例来演示你只需要把密钥、地址和模型名换成自己实际在用的即可。先封装一个 chat 函数避免后面重复写请求逻辑import json from llm_sdk import Client client Client( base_urlhttps://your-endpoint.example.com/v1, api_keyyour-api-key ) def chat(messages, toolsNone): kwargs {model: model-name, messages: messages} if tools: kwargs[tools] tools resp client.chat.completions.create(**kwargs) return resp.choices[0].message这里有一个小白经常误踩的点不要把 api_key 直接硬编码在最终要提交的代码里Demo 可以临时用生产环境必须通过环境变量或密钥管理服务读取。否则代码一旦被推到公开仓库密钥就等于送人了。3.2 定义两个最常用的工具天气查询与计算器为了让第一个 Agent 结构清晰我选了“查天气”和“算算术”这两个工具。一个代表外部 API一个代表本地计算函数。模型并不能真的执行它们只会根据工具描述生成调用意图。先说 Python 函数def get_weather(city: str) - str: # 这里应该请求真实天气服务Demo 里直接返回假数据 return f{city}晴23摄氏度微风 def calculate(expression: str) - str: try: result eval(expression) except Exception as e: return f计算出错{e} return str(result)注意在实际项目里不要用eval执行用户随便传来的表达式这里只是演示简单场景生产环境建议用安全解析器或只允许固定运算。然后定义工具描述这是指导模型的关键。每个工具需要参数名、类型、描述、是否必填tools [ { type: function, function: { name: get_weather, description: 查询指定城市的天气情况参数 city 是城市名称, parameters: { type: object, properties: { city: { type: string, description: 城市名例如北京、上海 } }, required: [city] } } }, { type: function, function: { name: calculate, description: 计算简单的数学表达式比如加法、乘法, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 3 4 * 2 } }, required: [expression] } } } ]工具描述里最重要的不是篇幅而是参数口径。如果模型生成的参数总是出现字段名拼写错误先检查你的描述是否足够明确。例如“城市名”三个字听起来简单但如果用户说的是“上海的天”模型可能犹豫是 city 还是 location描述里重提一次“期望字段为 city”效果会好很多。3.3 实现主循环模型决定、执行工具、观察结果、再决定完整的 Agent 主循环其实只有几行关键在于循环条件。我在本地实现后整个循环逻辑可以浓缩成下面这段def run_agent(user_input: str, max_iterations: int 5): messages [{role: user, content: user_input}] function_map { get_weather: get_weather, calculate: calculate, } for step in range(max_iterations): msg chat(messages, toolstools) if not msg.tool_calls: return msg.content tool_messages [] for tool_call in msg.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) result function_map[fn_name](**fn_args) tool_messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) messages.append(msg) messages.extend(tool_messages) return 已达到最大迭代次数未完成最终回答执行流程不复杂第一步把用户问题发给模型第二步检查回复里有没有 tool_calls没有就说明模型已经给出最终答案直接结束如果有就逐个执行函数并记录每个结果第三步把模型回复和工具结果追加到 messages 列表重新调用 chat第四步重复直到模型不再要求调用工具。刚才这段代码里我给 max_iterations 设了默认值 5防止模型陷入无限循环。这里有个角色归属必须注意每条工具结果消息的 role 必须是 “tool”而且必须带上 tool_call_id否则服务端会认为消息链断裂报错“工具调用 ID 不匹配”。项目实践中我看到大批新手在这里折腾半小时最后发现只是把工具返回消息挂错了角色。3.4 第一个 Agent 必然会踩的几个坑第一模型给出的参数类型和你的函数签名不匹配。模型是按 JSON Schema 来生成参数的但偶尔也会把数字写成字符串甚至把单个字段生成成数组。例如计算器工具期望 expression 是字符串模型可能生成一个列表。稳妥做法是在函数内部做一层参数转换和类型校验而不是信任模型的每一步输出。你可以在调用前打印 arguments肉眼核对一下时间长了就能摸清模型在这个工具上的“脾气”。第二工具函数本身报错会导致循环卡死。如果 get_weather 在请求真实服务时超时异常会直接中断程序。正确做法是让工具函数永远返回一个文本结果哪怕是“接口超时”这样模型还能基于这个结果调整策略。不要在不该 throw 的地方 throwAgent 主循环最怕未知异常把整个流程打断。第三无限循环和 token 浪费。模型有时会反复调用同一个工具特别是在工具描述不清晰的时候。除了限制 max_iterations你还可以记录调用次数一旦超过阈值就让模型“直接生成一个基于已有信息的答复”这样用户体验也不会太差。不要小看这一步真实环境中一个失控循环可能会让单次请求的 token 消耗暴涨几十倍。第四安全边界。给 Agent 的工具权限越大风险越大。如果工具里有“执行 shell 命令”或“删除数据库记录”这类高危能力至少要加一个确认流程。新手很容易为了演示效果把权限放得很宽导致模型误操作。我的习惯是高危工具一律做成“先返回待确认操作给用户用户说确认才真正执行”。4. 手写第一个 RAG从文档到回答的完整链路4.1 先把流程跑通加载、切片、向量化、检索、生成RAG 的代码量其实也很小。我习惯让新人在一个单文件脚本里做完五个步骤先不去管复杂框架。这里用一个简化示例假设你已经把文档读进来并做了切片然后调用 embedding 服务得到向量。如果你完全不知道 embedding 是什么可以把它理解为“把文本变成一个数字数组”语义相近的文本数字数组在空间中的距离也相近。这个数组就是“向量”。后续检索本质上是在这些向量里做最近邻搜索。一段极简的检索代码如下import numpy as np def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def retrieve(query_vector, document_vectors, top_k3): scores [ cosine_similarity(query_vector, doc_vector) for doc_vector in document_vectors ] top_indices np.argsort(scores)[-top_k:][::-1] return [documents[i] for i in top_indices]这段代码只做了一件最重要的事把问题向量和每个文档向量做余弦相似度比较取得分最高的 Top-K 个文档片段。在数据量不超一万条、对性能要求不高的情况下这种方法完全够用。等数据多了再考虑加向量数据库或者逼近搜索索引千万不要一上来就上重武器。4.2 切片策略与向量模型选型切片是所有 RAG 项目里最容易被低估的环节。切得太大会把多个主题混在一起模型检索时会得到大量噪音切得太小会让片段失去上下文模型看着半句话根本无法作答。我实测比较多的小白项目效果不好的第一原因不是向量模型不行而是切片太随意。第一优先按语义边界切片。如果文档有章节标题可以先用标题切割如果只是纯文本可以使用固定长度加重叠窗口。例如每段 500 个字符重叠 100 个字符这样既能控制长度又能避免把完整句子从中间切断导致信息丢失。第二向量模型选型的核心不是“越贵越好”。在中文场景里要看你手头文档的领域技术手册和日常对话差异很大。最靠谱的方法是准备二十个典型问题分别用候选模型跑一轮检索看哪个召回更符合预期。别急着选付费模型先用本地开源的极小模型验证流程。第三特殊格式的文档必须提前处理。PDF、扫描件要经过文字识别转成纯文本否则向量模型只能看到乱码。很多新人以为是自己向量化技术不对其实是源文档里的信息根本没被正确提取。4.3 检索到资料之后怎么组装 Prompt 才不白搭很多新手做完检索直接把几段原文大量塞进提示词结果模型回答还是不如预期。原因在于提示词里没有说清楚“资料用在哪、查不到怎么办”。我建议的最小模板是这样请阅读下面的参考资料然后回答用户问题。 参考资料 {context} 用户问题{question} 要求 1. 只能基于参考资料回答。 2. 如果参考资料中没有答案请直接回答“资料中没有相关信息”。 3. 不要编造参考来源没有的内容。第 2 条尤其重要。模型有很强的补全倾向面对缺失信息也会强行生成一个看起来合理的答案。你必须在提示词里明确允许它“认怂”。我在多个项目里测试过加上这句话之后回答的无中生有率显著下降。组装时还应该保留来源标题或编号例如在 context 里标注“[1] 来自产品手册...”这样后续如果需要做引用溯源模型能直接给出出处。即使当前只是 Demo也建议养成这个习惯。4.4 把五个步骤串成最短可用代码把上面的内容串起来一个最小 RAG 主流程大概是这样的# 假设已经准备好 documents 列表 # 假设 embedding 接口返回向量列表 query 产品支持离线缓存吗 query_vec embed([query])[0] matched retrieve(query_vec, doc_vecs, top_k3) context \n.join([f[{i1}] {doc} for i, doc in enumerate(matched)]) final_messages [ {role: system, content: 你是一个严谨的客服助手。}, {role: user, content: template.format(contextcontext, questionquery)} ] answer chat(final_messages) print(answer)整个过程没有引入任何大型框架只用列表、循环、相似度计算就能看到效果。对小白来说先把这条链路跑通再去了解那些现成的 Agent 和 RAG 框架会容易十倍。因为你已经知道每一步在干什么再去看框架时面对的就不是黑盒而是一堆你认识的组件的自动组合。5. 实战问题与排查技巧实录5.1 检索结果明明有相关句子模型还是答错这是我最常遇到的问题。现象是RAG 检索出的 Top-K 文本里确实包含答案但模型最终回答却南辕北辙。逐层排查后我发现主要原因有三个。一是检索到的片段被拼接后顺序混乱模型没有建立上下文。建议在 context 里按原始位置重新排序而不是按相似度从高到低排。二是提示词里没有强制要求“优先使用引用片段原文”模型可能把片段当作旁证自己重新推理。三是最新版模型对长上下文的注意力分配偶尔会失效片段夹在太多无关内容里重要信息被淹没。解决办法也简单降低 Top-K只保留最相关的两到三个片段。如果你还想进一步定位可以把检索到的片段单独发给模型只问“上述哪些片段蕴含答案”这样能区分是检索问题还是生成问题。5.2 Agent 工具调用时反复出现参数错误我在调试一个订单查询 Agent 时模型总是把订单号参数传成数组而接口希望是普通字符串。第一次遇到时我以为模型坏了直到检查工具描述才发现问题我在描述里写了“订单号可能有多个请用数组”。模型于是严格遵守了描述。这提醒我工具描述中的每个词都会被放大理解含义必须精确不要用模糊的复数表达。另一个原因是函数内部签名和 JSON Schema 不一致。建议把函数的入参定义、JSON Schema、实际接口字段三者放在同一个文件里命名保持一一对应。如果你改了函数签名却忘了改 Schema模型生成的参数执行时必然报错。给我的自查清单先打印模型生成的 arguments再手动调用工具函数最后才把结果写回消息链。三步下来90% 的参数错误都能定位。5.3 Agent 不停循环最后把 token 烧完了新手最心疼的就是这幕跑一个看似简单的任务模型连续调了七次工具对话费用蹭蹭上涨。其中最常见的原因是前面工具返回错误后模型没有足够信息修正于是继续尝试同一个动作。好比一个人站在钥匙面前却反复按门铃因为系统没有告诉他“钥匙不对”之外还能怎么办。应对方案有三个。第一设置硬性轮数上限超出即停止第二工具返回里追加一条“你可以尝试以下替代方案”的提示例如“天气接口超时请询问用户是否改用昨日数据”把模型往合理路径上引第三记录已调用过的工具组合当同一组合重复出现两次时由外层程序强制返回当前信息。这些技巧不复杂但能把失控概率下降一个量级。5.4 成本、延迟与安全速查表对于刚刚上手的项目我整理了一张问题排查速查表可以直接照着判断表现可能原因优先排查方向检索返回很多垃圾内容切片过大或没切分增加重叠、按语义边界切片回答内容来自模型脑补提示词未限制加上“资料没有就回答不知道”工具参数频繁报错Schema 描述不清晰精简描述统一字段命名循环次数异常工具结果无引导信息返回结果中加“下一步建议”调用成本飙升Top-K 太大或轮数太多降 K、截断工具返回、设上限敏感操作被模型执行工具权限过大增加人工确认、限制高危工具这张表不是标准答案但可以当作你排查问题的起点。每次遇到奇怪现象先记录当时的输入、输出和中间步骤再拿表对照比自己瞎调参高效得多。6. 从小 Demo 走向工程化的一点经验6.1 把 Agent 工具做成一键注册模块当你手里有二十个工具再在函数里写一个硬编码的 function_map 就会很痛苦。工程化第一步是把“工具注册”抽象成一个装饰器把函数、Schema、权限、超时都绑定在一起。代码大概长这个样子register_tool(nameget_weather, description查询天气) def get_weather(city: str) - str: ...注册表会自动收集描述并生成 tools 列表运行时通过名称查表调用。这个设计改动不大但维护成本低很多新增工具只需要写一个新函数不用动主循环。等你哪天要加权限控制也可以在装饰器里统一加。6.2 从单轮 RAG 到多轮问答和混合检索Demo 常见的单轮问答只能解决一次提问但真实业务里用户会追问“那如果延迟到三天呢”要实现追问必须把前面几轮对话也带进检索上下文。一种做法是把历史对话整理成“用户上一问/模型上一答”的摘要再与当前问题拼接后检索。另一种做法是让 Agent 在规划阶段决定是否需要改写查询词比如用户说“那它支持的接口有哪些呢”这里的“它”需要解析回上文的固件版本。混合检索也是工程化的必经之路。向量检索擅长语义理解但关键词检索在代码、型号、人名面前往往更准。我常用的组合是先并行跑向量检索和关键词检索再用一个轻量重排程序把两个结果合并排序。刚开始你不需要做到这一步但理解这个方向能让你在面对检索效果瓶颈时知道从哪下手。6.3 最后说点我的真实感受Agent 和 RAG 之所以让很多小白程序员觉得困难是因为市面上教程要么只讲概念让人停在理论要么直接丢一堆框架代码让人照抄唯独缺少“从最朴素原理开始一步步实现”的路径。我自己也是反复折腾了几个项目才彻底想明白这些技术本质上都不复杂复杂的是边界条件。先跑通一个能读文档、能查天气、能做计算的最简系统再慢慢加需求你会发现所谓“大模型开发”其实和写普通业务代码没有本质区别只是把控制权从程序员手里部分交给了模型的判断。“收藏必备”这个标题里说的收藏我更希望你不只是把文章放进收藏夹而是今天就打开终端把第 3 节和第 4 节的代码原样跑一遍。跑通一次你对 Agent 和 RAG 的理解会超过看十篇原理文章。如果之后你在跑通流程时遇到奇奇怪怪的报错可以回到第 5 节的速查表先按表格排查再根据日志逐层定位。技术这东西动手后的半小时比阅读很多天的收获都大。