从第5天开始,Agent学习终于进入深水区。前两天还在熟悉基础模型的调用方式,Day05-06直接切换到“怎么让模型自己干活”的核心问题。这两天的学习量很大,但收获也最明显:我把一套完整的Agent工作流从设计到实现完整走了一遍。这篇文章就对这两天的内容做一个复盘,重点讲我踩过的坑和一些可以直接复用的套路。如果你也在自学Agent开发,或者打算从API调用转向智能体应用的开发,这篇内容应该能帮你少走不少弯路。
1. 为什么Day05-06要聚焦这两块内容
1.1 从“会聊天”到“会干活”的关键跨越
很多人最初接触大模型时,觉得AI就是“你说一句,它回一句”,顶多是个更聪明的搜索引擎。但Agent学习的核心,恰恰是要打破这个被动交互的模式。Day05-06的安排,是在基础模型调用熟练之后,马上切入Agent最本质的东西:让模型拥有感知、决策、执行、记忆的闭环。
普通对话和真实Agent的最大区别,就是有没有“行动”。对话场景里,模型只需要输出一段让用户满意的文字;Agent场景里,模型必须根据用户目标,拆解出步骤,选择并调用外部工具,再根据工具返回的结果调整下一步动作。比如用户说“帮我把今天的会议记录整理成周报”,普通对话只能给出一段整理好的文本,但Agent会先去找到会议记录文件,读取内容,按周报格式生成文本,然后保存到指定位置,甚至把周报发给相关人。
这个跨越看起来只是多接了几个接口,实际上带来了一系列连锁问题:模型怎么知道该调用哪个工具?工具参数怎么传给模型?工具返回错误时模型如何恢复?用户目标太大时怎么拆解成多个步骤?这些恰是Day05-06重点解决的。
我自己最初的想法是先去做RAG或模型微调,觉得那是“大头”。后来发现方向反了。如果没有把Agent的骨架搭起来,RAG做出来也只是一个“带检索的聊天框”,而不是一个能独立完成任务的智能体。所以Day05-06把重心放在了Agent的运行机制和最小实现上,后续再往里面加记忆、加多Agent协作,都会顺很多。
1.2 学习路线的取舍:不贪多,先把骨架搭起来
这两天学习材料不少,很多教程一上来就是各种高级框架、分布式Agent编排、复杂工作流。我一开始也跟着学,但很快发现消化不良:名词太多,概念之间的关系没理清,手也动不起来。
后来我调整了策略:彻底扔掉“框架优先”的思路,回到最原始的执行逻辑。先搞清楚一个Agent自己运行起来,最少需要哪几部分,然后用最朴素的代码一步步拼。事实证明这个决策非常正确。
如果类比建房子,我前两天学的模型调用是“搞懂了砖头和水泥怎么用”,Day05-06就是在打地基和立柱子。地基是Agent的思维循环,柱子是感知、行动、记忆几个模块。柱子立不起来,后面装修得再好看也会塌。
所以这两天的内容,我把它总结成两条主线:
- 主线一是Agent架构的整体拆解:感知层、决策层、执行层、记忆层分别解决什么问题。
- 主线二是最小可运行Agent的实作:不依赖任何重型框架,用基础模型接口加几个工具函数,让一个Agent真的能跑通“理解问题-拆解任务-调用工具-观察结果-继续执行”的完整循环。
这两条主线学完后,再去看各种开源框架时,思路会完全不同。以前看到的是“它提供了什么”,后面看到的是“它用哪种方式实现了哪个模块,为什么这么设计”。
2. 核心架构拆解:Agent到底由哪几块组成
2.1 感知层:怎么让Agent“看懂”请求和上下文
感知层是Agent接收外部信息的入口。普通API调用里,输入就是一段用户消息;Agent场景下,输入可能包含多种形式:一句自然语言指令、一份上传的文档、一段系统日志、甚至实时轮询到的状态变化。
Day05的一个重要练习,是让Agent处理“非结构化输入”。例如用户说“帮我看看今天下午有没有空会议室,有的话就订一个,把邀请发给A同学”。这句话里包含了好几个关键点:时间范围是“今天下午”,动作是“查询+预订+发送邀请”,对象是“A同学”,状态是“如果空闲才订”。如果模型只做一次问答,这很轻松;但如果Agent要把这个指令拆成多个步骤,就必须先从输入中准确提取这些约束。
我做了一个简单验证:直接让基础模型输出一个JSON结构,包含intent、entities、params几个字段,并要求模型把用户指令拆成结构化意图。效果不错,但有一个关键心得:提示词里必须给出明确的格式约束和少量示例。否则模型会自由发挥,导致后面的工具调用环节无法稳定对接。
感知层还有一个容易忽略的点:不是所有输入都需要完整交给模型。比如一个很长的文件,直接塞进上下文会导致注意力分散,也加剧费用压力。更合理的做法是先对输入做粗加工:截断无效内容、提取章节标题、把关键人名/时间/地点单独列出来,再把加工后的信息交给Agent决策。这一步有点类似人看邮件先扫一眼主题和正文摘要,而不是每一封都逐字读完。
2.2 决策层:ReAct模式与任务规划
决策层是Agent的“大脑”,负责根据当前状态决定下一步做什么。Day05学到的核心模式是ReAct——让模型交替进行推理(Reasoning)和行动(Action),然后观察行动结果(Observation),形成循环。
我用自己的话解释这个模式:普通对话是“想完了直接说”,ReAct是“想一步,做一步,看一步”。比如任务“整理本季度报销数据”,Agent先推理“我需要先找到报销文件”,于是执行一个搜索文件的操作;工具返回三个文件路径后,Agent继续推理“这三个文件里哪些是季度的”,于是读取文件头部信息;看到第二、三个文件符合要求后,Agent再决定“读取全部内容并汇总”。每一步的推理都基于上一步的观察,行动也因此有依据。
在代码实现上,ReAct循环的结构大致如下:
- 把所有历史消息(包括推理、行动、观察)拼进上下文,送给模型。
- 要求模型输出一个结构化的行动决策,包含
thought(为什么这样做)和action(要做什么)。 - 如果
action是调用工具,则执行对应工具函数,把返回结果作为一条“观察”消息加到上下文,回到第1步。 - 如果
action是最终回答,则结束循环,把回答返回给用户。
这个循环简单,但很容易失控。最大问题是:模型可能陷入“一直在推理,永远不行动”的状态。这时候系统提示词必须强制约束:当存在可执行工具时,必须优先选择工具;只有确认任务完成,才允许输出最终回答。此外,一定要设置最大循环次数,防止模型在异常情况下无限调用。
2.3 执行层:工具调用与动作执行
执行层是Agent能“动手干活”的关键,也是Day06大部分实操时间的去向。所谓工具,本质上就是一段函数:输入是明确的结构化参数,输出是可解析的结果。例如“读取文件”工具,入参是文件路径,出参是文件内容文本;“发送邮件”工具,入参是收件人、主题、正文,出参是发送状态。
让模型学会调用工具,重点有三件事:
第一,工具描述要写得清晰。不要用模糊的话,比如“读取文件”,而要注明参数格式、类型、取值范围。比如路径必须是绝对路径,支持 txt/md/docx 格式。模型是靠工具描述来理解“什么时候该用这个工具”的,描述不清晰,调用率就会下降。
第二,参数必须严格校验。模型输出的参数经常有幻觉:路径不存在、时间格式写错、中文标点混入。执行层必须对每个参数做校验,失败时要返回一个清晰的错误信息,让模型下次能纠正。常见的做法是,在工具函数里用try/except包住,把异常文案转换为“工具执行失败:原因xxx”的观察消息。
第三,工具返回结果要结构化。不要返回一整块杂乱文本,最好分成status、data、error等字段。这样模型在下一轮决策时,能快速看到是否成功、数据是什么、失败原因是什么。结构化的观察结果,能大幅减轻模型的理解负担,减少重复出错。
2.4 记忆层:短期上下文与长期记忆
记忆是Agent和普通对话程序最不一样的地方。普通程序可以用一个变量保存历史记录,但Agent的记忆要服务于“决策”,而不是简单回放。
短期记忆最直接的做法,就是把对话历史放进上下文窗口。但上下文窗口是有限的,而且每轮消息都全量塞给模型,费用和延迟都会不断增加。我这两天实验下来,发现一个比较实用的办法:只保留最近3-5轮关键消息,超过的部分压缩成一段摘要放在历史开头。这样模型既不失上下文,又不会超过窗口限制。
长期记忆对应的是需要跨会话保存的信息:用户偏好、之前的项目进展、环境配置等。Day06我测试了最朴素的长期记忆方案:所有重要信息都写入一个本地JSON文件,需要时用关键词或简单规则检索。比如用户之前提到“我习惯周五晚上开会”,这条偏好被写入记忆文件;下次Agent规划日程时,会先检查记忆文件里有没有时间偏好,再决定发会议邀请。
很多人一上来就准备上向量数据库、做embedding。但我的建议是,如果你的数据量没有上千条,先用关键词检索和规则匹配完全够用。等跑通了完整流程,再判断是否真的需要向量检索。过早引入重型组件,反而会把注意力从核心逻辑转移到基建维护上。
3. 实操记录:用纯手工方式搭一个最小可用Agent
3.1 场景选择:做一个“会议纪要整理Agent”
Day05-06最核心的产出,是一个真能跑起来的“会议纪要整理Agent”。场景选得比较小:用户传入一段会议原始记录,Agent自动完成“提取任务、整理待办、生成摘要”,并把结果保存成一个Markdown文件。
选择这个场景有三个原因。第一,输入输出边界清晰,不会陷入“什么叫整理好”这种模糊问题。第二,需要用到至少三种工具:读取文件、解析文本、写文件,能覆盖工具调用的基本形态。第三,结果可验证,跑完就能打开文件看效果,能带来实实在在的正反馈,对学习状态很重要。
我强烈建议自学的朋友先选一个这类“小而有边界”的场景,不要一开始就做“全自动数据分析助手”这种野心太大的东西。Agent能不能跑通,关键不在于模型多强,而在于任务边界和工具设计是否清晰。
3.2 定义工具函数
因为我是在本地实验,没有引入重型框架,工具就是普通的Python函数。在设计上,每个工具都遵循“参数简单、返回结构化”的原则。
下面是一个简化版的工具注册逻辑:
def read_file(path: str) -> dict: try: with open(path, "r", encoding="utf-8") as f: return {"status": "success", "data": f.read()} except Exception as e: return {"status": "error", "error": str(e)} def write_file(path: str, content: str) -> dict: try: with open(path, "w", encoding="utf-8") as f: f.write(content) return {"status": "success", "path": path} except Exception as e: return {"status": "error", "error": str(e)}为了让模型能正确选择工具,我还维护了一个工具清单,包含工具名、功能说明、参数格式、返回格式。这个清单会写入系统提示词中,同时也在上下文里给一个调用示例。
举个例子:
{ "tool": "write_file", "params": { "path": "/tmp/meeting_summary.md", "content": "今天讨论了..." } }工具函数的代码并不复杂,但有一处容易被忽略:参数类型必须严格。比如read_file如果接收一个None路径,Python会抛TypeError,而模型下一轮可能还搞不清原因。所以我统一在每个工具入口做了防御性检查,确保返回的是结构化的错误信息,而不是让异常直接打断循环。
3.3 编写决策主循环
主循环是整个Agent的心脏。它做的事情可以概括为:把所有的历史消息拼接到上下文,询问模型下一步行动,然后执行并返回观察结果,反复循环。
我用的是非常朴素的while循环,逻辑如下:
def run_agent(user_query): msgs = [system_prompt, {"role": "user", "content": user_query}] for step in range(max_steps): response = model_client.chat(messages=msgs) action = parse_action(response) if action["type"] == "final_answer": return action["answer"] result = execute_tool(action["tool"], action["params"]) msgs.append({"role": "tool", "content": json.dumps(result, ensure_ascii=False)}) return "超过最大步数,任务未完成"这个循环有几个关键细节。
一是我在所有历史消息里同时保留了模型自己的推理、工具调用和观察结果,这样模型能“看到”自己刚才做了什么,避免反复提出同一个行动。
二是parse_action用的是双保险:优先按JSON解析,如果JSON失败,再用正则匹配输出里的action: xxx和params: xxx部分。为什么这么麻烦?因为模型有时候会输出额外文字,比如“好的,我来处理”,如果不把这些噪音去掉,工具调用就失败了。
三是max_steps必须存在。我一开始没设置上限,结果碰到一次模型连续调了十几次工具,白白消耗了Token和等待时间。后来固定为8步,正常任务基本都够用,超时了就返回“需要人工介入”的兜底信息。
3.4 提示词设计与参数选择
这个小Agent的系统提示词我改了很多版,里面最核心的是三部分:
- 角色定义:你是一个自动整理会议纪要的Agent,目标是完成用户指定任务。
- 工具使用规范:当你认为需要读取或写入文件时,必须调用工具;工具调用必须用指定JSON格式;一次只能调用一个工具。
- 决策输出规范:给出两个选项,一是调用工具,二是输出最终回答,不能同时做。
其中“一次只能调用一个工具”非常关键。没有这条约束时,模型经常尝试在一个响应里同时调用多个工具,比如一边读文件一边写文件,这会让执行逻辑变得很复杂。作为最小实现,我更希望模型一步步来。
参数方面,我做了一组对比,发现temperature设置为 0 时工具调用最稳定,因为不需要创造性发散;但最终回答摘要会有一点机械。折中后我把大部分对话流程的温度设为 0.2,只对最后写摘要的步骤设为 0.4,既能保证工具调用稳定,又不太冷冰冰。
max_tokens同样需要控制。如果给的max_tokens太小,模型的行动决策可能会被截断,后半截输出就成了乱码;给太大又浪费。我实测下来,决策输出通常300-500 token足够,最终回答可以放宽到1000左右。
3.5 运行结果与效果观察
第一次跑通的时候,我盯着控制台看了好久。用户输入是一段模拟会议记录:
“今天讨论了新版本延期问题,前端资源不够,决定从其他项目临时借调一名开发;待办:周三前完成接口联调;王工负责更新排期。”
Agent的执行过程大致是:
- 第一步,读取原始会议记录文件。
- 第二步,生成结构化摘要,提取了三个关键待办。
- 第三步,调用写文件工具,把整理结果保存为
meeting_summary.md。 - 第四步,输出最终回答,告诉用户摘要文件和待办列表已经生成。
虽然每一步都很慢,中途还出现过一次工具参数写错的情况,模型读到错误提醒后自己纠正了路径。那种“模型自己纠错成功”的感觉,是学Agent最上头的瞬间。
这次实操也让我意识到,一个最小Agent并不需要多么复杂的组件,一个模型接口、几个工具函数、一个循环、一份提示词,就够了。但真实工程里,难点会转移到稳定性和边界处理上,这也是Day06后半段一直在做的事。
4. 两天里踩过的坑与排查方法
4.1 模型陷入“嘴上跑火车”的循环
第一天下午第一次跑完整流程时,我遇到一个特别典型的问题:模型一直在输出“我需要先读取文件”“我接下来要做的是……”“好的,让我想想怎么处理”,看起来逻辑通顺,却始终没有输出工具调用JSON。
排查下来,原因是系统提示词给了太多的“推理空间”。模型倾向于把推理过程写得完整漂亮,但这在Agent里是灾难。后来我把输出规范改成了“如果存在可以完成任务的工具,必须直接调用;只有在不需要任何工具时,才能输出回答”,同时给了一个“错误示范”和“正确示范”的对比。这个问题立刻缓解。
后续还遇到过一种变体:模型确实调用了工具,但工具完成得很好之后,它并不总结,而是继续调用同一个工具,造成重复劳动。解决方法是要求在观察结果后先判断“任务是否已完成”,如果已完成,下一轮必须给最终回答。
4.2 工具参数解析总是失败
这是Day06最磨人的问题。模型返回的工具调用大概是这样:
{"tool": "read_file", "params": {"path": "/tmp/会议记录.txt"}}但偶尔它会在JSON前后加一堆解释文字,比如 “好的,我将会读取该文件:{...}”。直接用json.loads必然报错。我最初想靠严格提示词解决,但实测下来,无论提示词多严格,模型都可能在边缘情况下夹带私货。
最终的解法是双保险解析:
- 先用正则找到
{到}之间的部分,截取出来。 - 再尝试
json.loads;如果失败,把里面的中文引号和转义符修复一遍。
同时,我在工具调用示例里特意给了“带解释文字”的反例,帮助模型理解必须原样输出JSON。这样处理之后,解析成功率从最初的70%提升到了95%以上。
4.3 长期记忆写不进、读不出
做长期记忆功能时,我把用户偏好直接存进了一个本地文本文件。一开始遇到的问题是:Agent在决策时根本不知道“还有记忆这回事”,每次都是直接忽略记忆去问用户或按默认参数执行。
后来我把记忆检索做成一个独立工具,叫search_memory,并把它的描述写清楚:“在历史偏好中搜索与当前任务相关的信息,如果没有相关信息,返回空列表。”这样一来,模型在接到一个新任务时,会先主动调用记忆检索,再决定是否继续问用户。
还有一个更隐蔽的问题:检索逻辑太粗糙,导致把无关偏好也捞了出来。比如用户说“周五开会”,记忆里有一条“周五交通拥堵,会议时间安排在下午更好”,模型就会去调整时间。解决方法是给每条记忆加标签,比如time_preference、meeting_habit、file_location、contact_name,检索时先按标签过滤,再按关键词筛选。这比纯向量检索慢一点,但胜在规则完全可控,也特别适合小型数据量。
4.4 上下文越堆越长,费用和延迟直线上升
第三天下午我观察到一个现象:循环跑得越久,单轮响应时间越长。原因很直接:我把每一轮的工具观察结果都原封不动地拼进上下文,导致上下文越来越长,模型处理成本随之飙升。
最简单的解法,就是做“历史裁剪”。我实现的策略是:只保留最近两轮的工具调用和观察结果,更早的内容压缩成一段摘要放在开头,比如“已完成的操作:读取了文件A,生成过两版摘要”。这个是纯文本拼接,效果已经够用,至少不会再出现上下文长度超限的报错。
但要注意一点:裁剪历史时要保留“重要的中间结论”。例如模型曾经从文件A中提取出“预算为5000元”,如果这段观察被裁掉了,后续模型可能会忘记这个数字。所以我在裁剪前会先让模型把关键信息同步到记忆文件里,再对历史做压缩。这样即使历史丢了,Agent也能通过调用记忆检索找回关键信息。
4.5 常见问题速查表
说白了,Agent开发里有大量问题不来自模型本身,而来自工程细节。我把这两天遇过的典型问题做成了一张速查表:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 模型一直输出分析但不调用工具 | 提示词约束太弱 | 强制要求存在工具时优先调用,并给正反示例 |
| 调用工具时一直在JSON前后加解释文字 | 输出格式不稳定 | 正则截取 + 容错解析 + 减少prompt中的干扰项 |
| 模型做已经完成过的动作 | 缺少对历史观察的利用 | 循环内保留工具结果,加“任务是否已完成”判断 |
| 内存检索结果不相关 | 检索策略太粗 | 加标签过滤,再做关键词匹配 |
| 上下文增长导致延迟升高 | 历史全量保留 | 历史裁剪 + 摘要压缩 + 关键信息外置记忆 |
| 工具返回错误,模型反复用同样参数重试 | 错误信息不够具体 | 返回结构化错误,写明哪一步失败、参数问题是什么 |
这张表我会继续补充,因为很多问题是会反复出现的。每次花时间在工程上做好兜底,后面跑新场景时都会轻松不少。
5. 下一步要补的功课,以及这两天学习最直接的体会
5.1 下一步要补的功课
两天学完,我对Agent的“骨架”已经心里有数,但这还只是开始。我最想补的第一块,是如何让多个Agent分工协作。现在做的单Agent,所有任务都压在同一个模型循环里,一旦任务复杂,工具数量超过几十个,模型的选择困难会明显增加。多Agent分工的好处是每个Agent专注一个子任务,决策和工具调用都会更稳定。
第二块是系统性评估。我现在验证一个Agent好不好,基本靠肉眼观察输出是否合理。这太不严谨了。后面至少要做一批测试用例,覆盖正常、异常、边界场景,记录成功率、平均调用步数、无效调用比例等指标。只有量化了这些数据,才能知道改一次提示词是变好还是变差。
第三块是安全与容错。Agent一旦拥有写文件、发消息这类真实操作权限,一个错误决策就可能造成实打实的损失。所以后面必须给工具加上权限分级、二次确认、操作日志,确保Agent的行为可审计、可回滚。
5.2 学习体会:工程化细节才是真正的分水岭
这两天最直接的体会就是:Agent能否跑通,决定因素不是模型聪明不聪明,而是你愿不愿意把每一个环节的细节抠到位。
模型能力再强,如果你不告诉它有哪些工具、每个工具怎么用、输出格式是什么,它照样会瞎猜。反过来,只要你把工具描述、参数校验、循环约束、历史管理这些工程细节做好,即便用很基础的模型,也能跑出稳定可用的Agent。
我见过很多人一上来就追最新框架、最新模型,结果核心逻辑没搞懂,遇到问题只会换框架换模型。但我的亲身经历是,回归到最小实现,亲手把每一个模块写一遍,才能真正理解Agent为什么是今天这个样子,以及各种框架想帮你解决什么问题。
最后再分享一个小建议:如果你也正在学习Agent,请一定给自己留出大量的调试时间。不要指望第一次跑通就是完美结果,更不要因为一次失败就怀疑路线。我这两天大概有60%的时间都在盯日志、改参数、调提示词。但正是这些“磨人”的时刻,把Agent的原理一点点刻进了我的直觉里。这种手感,光看文章和视频是永远得不到的,只有自己动手踩几次坑,才能真正变成自己的东西。