零基础手写AI Agent系统:揭开循环、工具与记忆的底层原理 我最早接触Agent这个词是在某个技术社群里看别人晒聊天截图。当时觉得这东西很神好像给模型一段提示词它就能自己拆任务、调工具、出结果。后来我花了两个周末完全没用现成的Agent框架从第一行代码开始手写了一个能调多个工具、带简单记忆的Agent系统才真正想明白它背后的机制一点都不玄拆开看就是“一个循环 一堆工具 一套记忆规则”。而后者正是这门《闪学it-零基础手写AI Agent系统课》想让你亲手体验到的东西。先说清楚这门课解决什么问题。现在网上聊Agent的内容很多但大部分是教你“怎么调别人的框架”。你照着敲完跑通了demo回头问自己一句“它内部是怎么决定先调天气接口再查日历的”大概率答不上来。这门课反着来不装框架用Python从零写一个能干活的最小Agent把每一行代码背后的设计逻辑讲明白。适合三类人完全没写过代码、但想搞懂Agent原理的纯新手用过LangChain一类框架但总觉得哪里隔着一层的人以及准备面试、想在自己的项目里真正掌控Agent行为的开发者。1. 为什么坚持“手写”而不是一上来就上框架1.1 框架的抽象掩盖了什么过去两年各种Agent框架层出不穷它们的核心价值是把你必须重复写的胶水代码封装好。但问题也出在这里框架帮你做完了所有重要决策而新手根本感知不到这些决策的存在。举个例子。你调用框架里的一个“Agent”对象传入工具列表然后让它跑一个任务。框架在背后至少替你做了这几件事组织系统提示词、把工具描述序列化成模型要求的格式、解析模型返回的工具调用指令、执行工具后把结果拼回对话上下文、控制最大循环次数、管理历史消息长度。任何一环出了问题你面对的是几层抽象栈之外的报错信息。想排查连“该看哪一层”都不知道。这就是我坚持手写的原因之一先亲手把每块积木都搭过一遍才有资格去谈论“我用框架会更快”。零基础者更需要这种“笨功夫”它能帮你建立直觉模型而不是停留在“这么写能跑”的玄学层面。1.2 手写一遍到底能获得什么手写Agent的价值我用三个词概括可控、可排错、可迁移。可控是你完全知道系统每个时刻在做什么。模型输出了一段奇怪内容你能立刻判断是提示词问题还是解析逻辑问题。可排错是指排查链路完全不依赖框架源码你熟悉的是自己写的几百行代码。可迁移是最容易被忽略的手写过一次核心循环之后你会发现市面上的框架——不管包装得多炫——核心都是同一套思路。以后再学任何新框架本质上是在复习你已经理解的机制。还有一个实际好处面试和项目答辩。一个手写的Agent系统可以从模型调用原理讲到工具协议再讲到记忆管理每一句话都是你实际做过的。这比“我熟练使用某某框架”有说服力得多。1.3 “零基础”的定位与课程入口设计零基础不代表完全没有前置知识。著完整实现一个Agent至少需要Python的基础语法、函数定义、字典操作、JSON处理能力和最基础的网络请求概念。这门课的入口设计是这样前几章先用一系列短平快的练习把Python补到“够用”水平不追求写出多优雅的代码会读、会改、会查错就行。课程里常用一个比喻——把Python基础当成“学会骑自行车”而Agent系统是一辆可以自己换挡的电助力车。你不需要会造发动机但必须会踩踏板、能看懂仪表盘。这样设计是为了让零基础的人不至于被开头庞大又枯燥的语法淹没尽早进入能跑通程序的“正反馈循环”。2. 一个Agent最少包含哪几块先搭出运行骨架2.1 Agent的本质一个带工具的while循环很多人把Agent想得很高级其实它的运行本质就是一个循环while 系统还没给出最终答复: 把对话历史发给大模型 如果模型返回的是工具调用指令: 执行对应工具 把工具结果放回对话历史 如果模型返回的是最终答复: 结束循环这就是全部骨架。所谓“手写Agent”核心就是亲手把这个循环写出来再把每一环的细节填满模型调用、工具描述、结果解析、上下文拼接、终止条件。聊到这里我们应该回到一个更根本的问题大模型本身并不能执行任何动作。它只能“生成文本”。Agent的意义就是给这个“只会说话的模型”接上手脚——通过约定好的协议让模型以输出结构化文本的方式“请求”调用某个函数而函数真正由你的代码执行。模型是大脑你的代码是手脚。2.2 第一版骨架纯粹的对话循环为了讲清楚我给出一个最简版本的循环逻辑。它没有任何工具只是不断接收用户输入、交给模型、打印返回。这是所有Agent的地基。def chat_loop(model_client): history [] print(对话开始输入 q 退出) while True: user input( ) if user.lower() q: break history.append({role: user, content: user}) reply model_client.chat(history) history.append({role: assistant, content: reply}) print(AI:, reply)这里的model_client是一个封装了模型API调用的对象实际使用时替换成你所接入平台的SDK或HTTP请求即可。关键在于理解history这个列表它保存了完整的对话记录每次请求都带上它模型才能知道前面聊过什么。这个骨架虽然简单但它已经把循环结构搭出来了。下一步是让循环具备“判断能力”——模型不再只是回复文本而是可以输出“我要调用某个工具”的指令。2.3 加入工具让模型输出结构化指令现在我们的循环要升级。约定一个协议当模型认为需要查天气时它不会直接说“我来查一下天气”而是输出一段特殊结构例如{tool: get_weather, args: {city: 北京}}而你的代码负责解析这段结构找到对应函数传入参数并执行再把结果追加回history。示意如下def agent_step(model_client, history, tools): reply model_client.chat(history) # 模型返回文本 instruction try_parse_tool_call(reply) if instruction is None: return reply, True # 没有工具调用结束 result tools.execute(instruction[tool], instruction[args]) history.append({role: user, content: f工具执行结果{result}}) return None, False # 继续循环注意几个设计细节。第一工具结果包装成role: user的消息追加进历史这是常见做法让模型以“看到了新的客观信息”的状态继续推理。第二使用try_parse_tool_call而不是直接json.loads是因为模型经常在JSON前后夹带解释文字必须做容错提取。第三循环不是无限跑的必须在外面包一个最大轮次限制防止模型反复调用工具不收敛。2.4 骨架阶段最容易踩的三个坑这个阶段新手普遍会遇到三个问题我提前说省得你踩同一个坑。第一个坑循环永远不会主动结束。模型有时会一直输出工具调用指令你的循环就跟着一直转。解决方案是在循环条件里加两个约束——最大迭代次数比如10次以及“当工具调用是重复的同一个请求且参数相同”时的去重或终止逻辑。第二个坑上下文无限膨胀。每执行一次工具就把一整段结果塞进history对话轮次一多请求体越来越大费用和延迟一起涨。解决办法是后面的“记忆管理”章节——限制只保留最近几轮消息更早的做压缩或丢弃。第三个坑模型返回的JSON不合法导致解析直接报错。这种情况太常见了模型输出的可能是json开头包裹的代码块或者JSON里混进了说废话的文本。所以解析函数里必须做“取到第一个{和最后一个}截取中间内容再解析”的容错操作这是每个手写Agent的必要工序。3. 工具调用是实现“手写”的关键搞懂Function Calling机制3.1 模型到底是怎么“调用”工具的弄清楚一个概念模型并不会真的去执行函数。它只是在生成文本时按照训练和提示的要求“选择”输出一段符合工具调用格式的文本。你的代码负责拦截这段文本、解析、执行、把结果送回去。比较正规的做法是使用模型服务商提供的Function Calling能力也就是在请求参数里声明tools列表模型收到后会倾向于输出一个结构化的工具调用对象而不是普通自然语言。手写Agent时可以把这层“声明工具”的动作也自己控制这样框架对你来说就没有秘密了。给你一个直观的对比不声明工具时你问模型“北京今天适合出门吗”它会直接给你一段建议声明了get_weather工具后模型会先输出“我需要调用get_weather参数是北京”然后等你的代码把真实的天气数据给它再根据数据组织回答。后者之所以更有用是因为它把“知识性回答”和“实时数据查询”分开了。3.2 工具描述怎么写模型才不容易选错工具调用能不能做好一半的功夫在工具描述上。描述写得模糊模型就会在几个工具之间犹豫甚至乱选。写工具描述有三条经验。第一名称要动词开头、含义单一。get_weather比weather_info好send_email比email_operation好。工具就是函数让它像函数一样命名。第二描述里要写清楚“这个工具能做什么、适合在什么场景下用、不该什么时候用”。这是给模型看的“使用说明书”。举个例子一个search_web工具描述不要只写“搜索网页”而要写“当用户问题涉及实时信息、最新新闻、或本地知识库中没有的领域知识时使用此工具获取互联网信息如果只是想闲聊不要使用”。第三参数定义要精确。用JSON Schema风格声明每个参数的名称、类型、是否必填、取值范围。参数说明越具体模型“猜参数”的余地就越小。比如城市参数最好写明“使用中文城市名如北京、上海”模型就不容易填成拼音或英文。3.3 工具结果的回填比你想的更讲究很多初学版本是这样处理工具结果的把结果用字符串拼一下直接塞进对话历史。这没问题但有几个细节影响效果。第一个细节结果要带“来源标签”。假设工具查回来的数据是{city: 北京, temp: 23}你直接拼成“北京 23”丢回去模型容易把“北京23”当成对话的一部分混着理解。更好的方式是拼成“工具get_weather返回结果城市北京温度23度”明确告诉模型这是工具返回的结构化信息不是用户说的话。第二个细节结果为空或出错也要回填。如果工具执行抛异常不要中断整个Agent而是把错误信息拼到历史里让模型自己决定下一步。很多情况下模型会换个参数重试或者直接告诉用户“这个功能暂时不可用”。第三个细节超大结果要截断。一个查询可能返回几千字的内容全部塞进对话会导致token暴增还会稀释模型对用户的注意力。我通常的做法是结果超过一定长度就先截断或做摘要只保留最相关的部分。3.4 实战示例写一个“查天气并做日程建议”的工具我把上述环节串成一个完整示例。目标是让Agent具备一个能力用户说“明天北京适合户外跑步吗”Agent自动调用天气工具再根据结果组织回答。# 工具定义用字典表示方便传给模型 TOOLS [ { name: get_weather, description: 查询指定城市未来几天的天气情况。当用户询问天气、出门是否合适、穿衣建议时使用。, parameters: { type: object, properties: { city: { type: string, description: 中文城市名例如北京、上海、广州 } }, required: [city] } } ] # 实际执行函数 def get_weather(city): # 这里是示意真的项目中替换为天气服务接口调用 return {city: city, temp: 26, condition: 多云, wind: 3级}接下来是Agent主循环。核心逻辑就是把TOOLS随请求发给模型模型返回若是工具调用就执行并把结果回填否则输出最终回答。这个流程在课程里会配合完整的带日志调试版代码教每一轮迭代都打印出“当前模型输出、解析结果、工具返回值”让你肉眼看到Agent的推理过程。我当时第一次真正跑通这个链路时最强烈的感受是原来“智能感”只是协议的功劳。模型没有变聪明是我给它搭了一个“先用工具、再下结论”的舞台它才展现出了类似规划的能力。4. 记忆问题把上下文管理从“塞纸条”升级到“三层笔记系统”4.1 为什么非做记忆管理不可明确一件事大模型的上下文窗口是有限的。即便窗口变大也不是让你把所有历史都往里塞。用“全量历史全文工具结果”的方式跑Agent三轮以上对话就明显变贵、变慢还容易“走神”——早期不重要的细节会干扰模型对当前问题的判断。更贴近实际的困境是如果你做了一个“个人助理Agent”用户早上说“我明天要去杭州出差”下午问“帮我规划一下出差的行程”Agent必须记住上午的信息。如果上下文里已经没有这句话了它就彻底失忆。这就逼着你设计记忆系统。我的课程把记忆设计成三层对应不同生命周期短期记忆负责当前这轮任务的上下文中期记忆负责最近几轮对话的要点长期记忆负责跨天跨会话的持久信息。4.2 短期记忆滑动窗口的实现最简单有效的是滑动窗口方案。维护一个消息历史列表每次请求前只取最后N条发给模型。N取多少取决于你使用的模型上下文窗口大小和你的成本预算。早期可以做小一点比如只保留最近十轮以内的消息。代码示意MAX_HISTORY 10 def build_request_messages(history): return history[-MAX_HISTORY:]不要小看这一行逻辑。很多Agent的“智商下降”不是模型不给力而是你把一两百轮前的唠叨话全塞给了它它分不清轻重。滑动窗口虽然粗暴但它保证了“最近的对话永远以完整形态出现”对有明确目标的多轮任务来说这比什么都重要。4.3 长期记忆摘要记忆与关键词检索只靠滑窗隔天的信息还是会丢。这时候需要长期记忆。不掉入向量数据库的重型方案前一个朴素的长期记忆做法是“摘要归档”每一轮或每个任务结束后用一个单独的模型调用把这段对话压缩成一两句话的摘要连同时间和标签存进一个JSON文件。下次用户提到相关话题时先把历史摘要检索出来按相关度挑几条拼进提示词。这里就有“检索”的需求了。零基础阶段不需要先上向量库可以做最简单的关键词匹配——把摘要里的词和用户当前问题里的词做交集按匹配数排序。效果够用逻辑还简单def retrieve_memories(memory_list, query, top_k2): scored [] for mem in memory_list: score len(set(mem[keywords]) set(query)) scored.append((score, mem)) scored.sort(reverseTrue, keylambda x: x[0]) return [mem for score, mem in scored[:top_k] if score 0]等这个版本的检索跑通再去理解“向量检索”的底层思想就顺理成章把文字变成坐标从“基于词面重合”升级到“基于语义相似”。原理上是一样的都是“找相关内容”。4.4 记忆的保存与恢复一个真正的Agent系统关了程序再打开不能把前一天的对话全忘光。所以记忆要落盘。简单方案是存成JSON文件启动时加载运行时更新任务结束时保存。import json class Memory: def __init__(self, pathmemory.json): self.path path self.data self.load() def load(self): try: with open(self.path, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return {short: [], long: []} def save(self): with open(self.path, w, encodingutf-8) as f: json.dump(self.data, f, ensure_asciiFalse, indent2)这里有个实操细节写入时设置ensure_asciiFalse否则JSON里全是\u开头的中文转义你手动翻阅日志时根本没法读。这也是很多教程不会提的小坑。5. 从“单次问答”到“多步任务”任务规划的正确打开方式5.1 什么任务才需要规划不是所有问题都要任务规划。用户问“你好你是谁”直接回答就行规划反而画蛇添足。但一个任务是“分析这份销售数据找出下降最多的地区并写一份简要报告”一次模型调用解决不了它需要读文件 - 理解结构 - 做筛选或者计算 - 组织结论。这个过程天然是多步的。多步任务的核心是拆解。而Agent的规划能力本质上还是借助模型去“生成一个待办清单”然后Agent循环去逐项执行。拆解本身是模型的本事逐步执行和进度跟踪则是你的代码逻辑要承担的事。5.2 ReAct思想的手写实现业界有个经典框架叫ReAct核心就是让模型交替进行“思考”Reasoning和“行动”Acting。翻译成代码循环就是每一步都要模型先回答一个问题“基于当前情况下一步该做什么需要用什么工具还是不调用工具直接回答。”手写实现时我把一个任务重构成如下的循环状态task_status { plan: [], current_step: 0, results: [], completed: False }每一轮迭代时把任务目标、已完成的步骤结果、下一步的候选动作一起发给模型。模型输出“下一步我打算做什么”代码负责执行执行结果再放回状态继续下一轮。直到模型声明“所有步骤已完成我可以生成最终答复”。这个方案比“一次给出全盘计划然后闷头执行”要稳健。因为实际执行中每一步都可能出现意外——文件不存在、接口超时、数据格式不对如果在计划阶段就把后面所有细节都定死遇到一个意外整个计划就作废了。边想边做、边做边调整才是更符合真实场景的方式。5.3 一个完整的示例数据分析Agent我把多步任务落到一个具体场景。假设你的Agent要处理某个CSV文件里各地区的销售数据然后回答“哪个地区下降最明显”。第一步Agent先调用工具list_files看看目录下有哪些文件。第二步调用read_csv读取文件并提取列名。第三步调用calculate_sales_change计算出各地区的环比变化。第四步根据计算结果组织成一段自然语言回答。这里面每一步都可能出错而手写的好处就是你能在日志中清楚看到卡在哪一步。我在课程里会让学生为每个步骤添加“调试日志输出”这个习惯可以使问题排查的时间大幅下降。这里有一个值得思考的细节模型怎么知道第三步要算“环比变化”因为你在第二步的返回结果里给它看了列名和样例它在上下文中看到了last_month和this_month两列自己推理出了需要计算这两列的变化。这意味着让Agent“聪明”的关键往往不是模型本身选得多准而是你给它的中间结果够不够清晰。5.4 规划失败的两类典型问题这个阶段会碰到两类典型问题。第一类是“过度规划”。总想着一步到位把任务拆到很细结果拆出几十个步骤跑起来又慢又贵还容易在中间某一步失手。解决方案是加一道约束在提示词里写明“优先用最少步骤完成任务能用一步完成就不用两步”。第二类是“规划后不反思”。Agent按计划执行完但没有回头检查结果是否真正解决了用户问题导致回答空洞甚至错误。我的做法是在循环的最后强行加一个“验证节点”让模型对比“任务目标”与“当前收集到的信息”判断是否真的足够回答用户不够就继续补充够了才输出最终答复。这个验证节点能把不少“看似答了、实则没答”的情况拦在门外。6. 课程怎么带零基础的人走完这条路线6.1 阶段划分与每阶段交付物这门课在内容组织上花了心思我不赞成那种“三十天从入门到放弃”式的铺排所以把整个学习路线切成八个阶段每个阶段结束后都要求你交付一个能跑的东西。一阶段Python快速通关。不深挖语法原理用一系列小练习覆盖变量、列表、字典、函数、文件读写交付一个小工具能读CSV文件并统计行数的脚本。二阶段第一次发起模型对话。学会API调用方式、鉴权、流式与普通输出交付一个命令行聊天机器人。三阶段提示词工程。学会结构化输出、少样本示例、角色设定交付一个能按JSON格式输出的“信息提取器”。四阶段函数调用入门。理解工具描述协议与补全协议交付第一个能自己调工具的天气助手。五阶段记忆系统。实现滑窗、摘要归档、关键词检索交付一个“隔十分钟还记得你叫什么的助理”。六阶段多步任务规划。实现ReAct循环与验证节点交付一个能自动分析CSV数据并写结论的Agent。七阶段把Agent变成服务。用Web框架包装Agent实现一个网页聊天入口交付一个可以在浏览器里对话的完整应用。八阶段多Agent协作。把复杂的任务拆给两个角色Agent分工完成交付一个“写作校对”双Agent系统。6.2 每个阶段需要练的几个小项目纯粹跟着知识点走很容易“学过就忘”。所以每个阶段还要配两个小项目异常简单的那种五分钟能做完但能强迫你动手。练Python基础时写一个“通讯录”用字典存联系人函数负责增删改查。练API调用时写一个“翻译小助手”输入中文调用模型接口翻译成英文。练工具调用时把通讯录封装成工具让Agent能帮你“找到某人的电话号码”。练记忆时给Agent加一个“私人笔记”它记事之后你能随时问它“我之前记过什么”。这些小项目难度低到你不好意思不做但组合起来就展示了Agent完整能力的面貌。课程里始终贯穿着一个理念多写一行真实代码胜过背十句框架文档。6.3 什么时候才建议上框架我个人观点是至少完整手写过一遍Agent再考虑上框架。这不是说框架没用而是说框架的最大价值是省略重复劳动而不是帮你掩盖理解盲区。当你已经手写过循环、工具协议、记忆、规划之后再转头看框架你会看到一片新的景象你能说出某个框架的Agent类里大概使用什么样的循环它的记忆模块在什么场景下会失效它的工具调用解析器和你自己写的容错逻辑有何异同。到这时候你才真正拥有了选择权——既可以继续手写也可以愉快地用框架提升效率而不是被框架牵着鼻子走。课程后面有一节专门做“对比实验”拿同一个任务让学员先手写实现再引入一个主流框架做对照。多数人的感受是框架确实省代码但手写版本排错更容易因为任何一步出错你都瞬间知道问题在哪一层。7. 常见问题与排错速查表我整理了手写Agent过程中近乎必遇的几个问题之前踩坑时写在一张表里现在直接分享给你。现象典型原因排查思路修复方案Agent一直循环调用工具不给出最终答复缺少循环结束条件或条件过于宽松打印每一轮的模型输出观察是否重复同一工具调用增加最大迭代次数对重复的工具调用直接终止在提示词里要求“信息足够后必须直接回答”解析工具调用时JSON报错模型在JSON前后加了解释性文字、代码块标记或多余逗号打印模型的原始输出肉眼检查格式解析时截取第一个“{”到最后一个“}”之间的内容再做解析模型编造不存在的参数工具参数描述太模糊或没有给出取值范围检查工具参数说明看模型能自由发挥的空间有多大给每个参数明确的示例值、正反向描述并标注必填项工具执行成功了但模型回答时不当回事工具结果回填格式可读性差模型没识别出那是权威数据查看回填历史的消息内容与上下文其他消息对比回填时加“工具执行结果”前缀结构化展示字段和值对话轮次一多响应变慢、变贵历史消息无限制增长打印每次请求的消息数观察增长趋势引入滑动窗口只保留最近N轮Agent表现“记忆力差”总是忘记之前交代的信息长期记忆没有真正被检索并注入上下文检查记忆检索逻辑确认关键信息被取回完善关键词匹配或升级为向量检索并调试注入提示词的位置模型一个任务拆出几十个步骤跑起来又慢又乱提示词中没有约束步骤数量的偏好观察规划输出的结构判断是否超出必要提示词写明“用最少步骤完成任务”并限制最大规划长度部署到服务后并发情况下记忆互相串记忆对象是进程级全局变量多个用户共享了同一份数据检查记忆对象的生命周期按会话ID隔离记忆存储不同用户使用不同的记忆文件或键几个排查时的通用技巧也一并写在这里。给Agent加上“日志开关”是最重要的一步每一轮都打印出“模型原始输出、解析结果、工具执行结果”把运行过程完全透明化问题定位就成功了一半。先把代码跑通再去加各种能力和优化不要在第一版就追求完整。模型输出不稳定是常态你的代码要尽量容错别把“模型必须完美输出”当作可用条件。再分享一个容易忽视的小动作当你怀疑是哪一轮导致Agent行为异常时把那几轮的完整历史消息手工打印出来按顺序读一遍很多逻辑问题立刻就能暴露。别只盯着单独的某条消息看——上下文里的前后关系往往才是问题根源。我个人在实际操作中还有一个固定习惯每次跑通一个新功能先把当时的完整代码和一份“运行日志摘录”存档。后来我发现这批存档是我写这篇文章最宝贵的素材来源——很多当时觉得“换一种写法试试看”的调整事后看就是避坑的关键节点。如果你准备系统学习Agent强烈建议你也用同样的方式记录自己的调试过程这份一手资料比任何教程都值钱。