用做项目的方式啃面经:从零实现Agent开发核心模块
1. 从“背题”到“造物”为什么我劝你用做项目的方式去啃面经“从面经开始的agent开发学习”——这个标题乍一看像是个悖论。面经是别人面试后的回忆碎片agent开发是实打实的工程活这两件事怎么扯到一起我最初也这么想直到自己带过几个刚入行的朋友、又回头翻了一遍市面上流传的各类面经之后才意识到一个被大多数人忽略的事实面经里藏着这个领域最真实的“需求文档”。你去看那些关于agent岗位的面经高频问题翻来覆去就那么几类怎么设计一个能调用工具的agent记忆模块怎么存、怎么取多轮对话里状态怎么维护工具调用失败了怎么重试这些问题不是考官故意刁难而是真实项目里每天都要面对的决策点。换句话说面经是行业把“实际工作中最常遇到的坑”浓缩成了问答形式。你如果只是背答案那确实学不到东西但如果你把每一道题当成一个待实现的小需求动手把它跑通那面经就变成了一份现成的学习路线图。这篇文章就是写给那些想入门agent开发、但面对一堆框架和概念不知道从哪下手的人。我会以“面经驱动”的思路为主线把常见考点拆成可落地的模块一步步带你把它们实现出来。不需要你事先精通大模型原理也不需要你有多深的工程背景只要你会写一点Python、能看懂基本的API调用就能跟着走完。核心思路只有一句话别把面经当题库把它当需求清单每道题都动手做一遍。我自己的习惯是拿到一份面经先不急着看答案而是把问题抄下来然后问自己如果让我从零实现这个功能我会怎么设计需要哪些组件数据怎么流转边界情况在哪这个过程比看十篇解析文章都管用。接下来我会按这个逻辑把agent开发里最核心的几块内容拆开讲每一块都对应面经里的典型问题同时给出可运行的代码骨架和我在实操中踩过的坑。2. 把面经问题翻译成技术需求一份可执行的任务清单2.1 面经里的问题到底在问什么先拿几个高频面经问题开刀看看它们背后真正对应的技术能力是什么。“请设计一个能根据用户问题自动选择工具的agent。”——这道题考的不是你会不会调API而是工具注册、意图识别、参数抽取、结果回填这一整条链路。面试官想听的是你有没有想过工具描述怎么写模型才能选对参数格式不匹配怎么办选错了工具怎么兜底“agent的记忆应该怎么设计”——这题背后是短期上下文管理、长期向量存储、记忆检索策略三个层次。短期记忆就是对话历史怎么截断、怎么压缩长期记忆是把哪些信息存下来、用什么方式存、什么时候取出来用。“多轮对话中如何保持状态一致”——考的是状态机设计和会话隔离。你得想清楚哪些状态放在服务端、哪些放在客户端并发请求进来怎么不串号。“工具调用超时或返回异常怎么处理”——这是容错与重试机制包括超时设置、重试次数、降级策略、错误信息怎么反馈给模型让它重新决策。你看每一道面经题拆开之后都是一个具体的技术模块。把它们列出来就是一份学习任务清单面经问题对应技术模块核心难点工具选择工具注册与路由描述质量、参数校验记忆设计短期长期记忆截断策略、检索相关性状态保持会话管理并发隔离、持久化异常处理容错机制重试边界、降级方案多agent协作编排与通信角色划分、消息传递2.2 为什么按这个顺序学最省力有了清单接下来是排序。我的建议是先做单agent加工具调用再加记忆再加状态管理最后碰多agent。这个顺序不是随便定的它对应着依赖关系。工具调用是agent的“手”没有手agent只能聊天做不了实事。而且工具调用这条链路最短从用户输入到工具执行到结果返回一个函数就能跑通适合建立信心。记忆是“脑”的扩展但记忆的前提是你得有东西可记——对话历史、工具调用记录这些在工具调用跑通之后自然就有了。状态管理是“骨架”它把前面所有东西串起来保证多轮对话不乱。多agent是“组织”是多个单agent的协作单agent都没跑顺多agent只会让你调试到崩溃。我见过不少新手一上来就想搞多agent框架结果连单个工具调用的参数校验都没搞明白最后卡在“为什么模型总是传错参数”这种问题上。所以别急按依赖顺序来每一步都跑通了再往下走。2.3 环境准备别在第一步浪费时间动手之前把环境搭好。这里我只说最必要的几件事避免你在配置上耗掉半天。Python环境用3.10以上太老的版本有些库不支持。依赖管理用venv或者conda都行我个人习惯venv轻量。核心依赖就几个一个大模型调用库OpenAI SDK或者兼容接口的任意库、一个Web框架FastAPI足够、一个向量库初期用chromadb的本地模式零配置。如果你要用到本地embedding再加一个sentence-transformers。提示不要一上来就装一堆框架。LangChain、AutoGen这些确实方便但它们封装太厚出了问题你根本不知道是哪一层的事。初学阶段用最原始的API调用把链路跑通理解每一步在干什么比什么都重要。环境变量管理用python-dotenv把API key放在.env文件里别硬编码在代码里。这个习惯从第一天就养成后面能省很多事。3. 工具调用链路从“模型选错工具”到“稳定路由”的完整实现3.1 工具注册表的设计描述比代码更重要工具调用的第一步是让模型知道有哪些工具可用。大多数人会写一个函数列表每个函数带一个描述然后塞进prompt里。但实测下来模型选错工具90%的情况是描述写得不好。什么叫描述写得好我总结三条第一说清楚这个工具“做什么”而不是“怎么做”第二说清楚“什么时候用”和“什么时候不用”第三参数说明要具体到格式和取值范围。举个例子假设你有一个查天气的工具和一个查股票的工具。差的描述是“查询天气”和“查询股票”。好的描述是“根据城市名称查询当前天气状况返回温度和天气描述。当用户询问某地天气、气温、是否下雨时使用。参数city为城市中文名如‘北京’。”以及“根据股票代码查询实时股价返回当前价格和涨跌幅。当用户询问某只股票价格、涨跌时使用。参数symbol为6位数字代码如‘600000’。”你看好的描述里包含了使用场景的边界模型在决策时就有了依据。我试过同一个模型只改描述工具选择准确率从六成提到九成以上。工具注册表的代码结构大概是这样TOOLS [ { name: get_weather, description: 根据城市名称查询当前天气状况..., parameters: { type: object, properties: { city: {type: string, description: 城市中文名} }, required: [city] } }, # 更多工具... ]这个结构直接对应OpenAI的function calling格式换成其他模型也大同小异。3.2 参数抽取与校验模型传错参数怎么办模型选对了工具不代表参数就传对了。常见问题有三个参数名写错、参数类型不对、必填参数缺失。参数名写错通常是描述里没写清楚。比如你期望的是city模型传了location这就是描述里没明确参数名。解决办法是在参数描述里把名字再强调一遍或者在系统提示里加一句“严格使用给定的参数名”。参数类型不对更隐蔽。比如你期望一个整数模型传了字符串“123”。这种情况需要在执行前做类型转换和校验。我的做法是写一个校验层对每个工具的参数做一次检查def validate_params(tool_name, params): schema get_tool_schema(tool_name) for key, spec in schema[properties].items(): if key in params: expected_type spec[type] actual params[key] if expected_type integer and isinstance(actual, str): params[key] int(actual) elif expected_type string and not isinstance(actual, str): params[key] str(actual) return params必填参数缺失时不要直接报错给用户而是把错误信息返回给模型让它重新生成。这就是所谓的“自修复循环”。实测下来模型在收到“缺少参数city”的反馈后大部分情况能自己补上。3.3 执行与结果回填让模型理解工具返回了什么工具执行完之后结果怎么给模型这里有个坑不要把原始返回直接塞回去。比如你调用一个搜索API返回了一大段JSON里面什么都有模型看了反而懵。正确的做法是做一层结果格式化只提取模型需要的信息用自然语言描述。比如天气API返回了温度、湿度、风速、气压一堆字段你只需要告诉模型“北京当前温度25度晴”。这样模型在生成最终回复时就不会被无关信息干扰。结果回填的另一个要点是错误处理。工具执行失败时不要把异常堆栈直接给模型而是给一个简短的错误描述比如“天气服务暂时不可用”。模型收到这个信息后可以选择告诉用户稍后再试或者尝试其他工具。整个链路跑通之后你会得到一个这样的流程用户输入 → 模型决策选工具抽参数→ 参数校验 → 工具执行 → 结果格式化 → 模型生成回复。这个流程就是agent最核心的循环后面所有的扩展都是在这个循环上加东西。4. 记忆模块短期上下文与长期存储的取舍4.1 短期记忆对话历史不是越长越好面经里问“记忆怎么设计”很多人第一反应是“把历史都存下来”。但实测下来历史越长模型越容易跑偏。原因有两个一是上下文窗口有限塞太多会挤掉系统提示二是无关历史会干扰模型对当前问题的判断。短期记忆的核心是截断策略。我常用的有三种按轮数截断、按token数截断、按重要性截断。按轮数截断最简单保留最近N轮对话。N取多少看你的场景。客服类场景5到10轮够用复杂任务可能需要20轮以上。按token数截断更精确设定一个上限比如3000 token从最新往旧累加超了就丢。按重要性截断最复杂需要判断哪些历史对当前问题有用通常结合长期记忆来做。我一般用token数截断加一个保底系统提示永远保留最近3轮对话永远保留中间的按token预算裁剪。这样既不会丢关键信息又不会撑爆上下文。4.2 长期记忆存什么、怎么存、怎么取长期记忆解决的是“跨会话记住用户信息”的问题。比如用户上次说“我对花生过敏”这次点餐时agent应该能想起来。存什么不是所有对话都值得存。我通常只存三类用户明确陈述的偏好或事实“我住在北京”、重要的事件“上周投诉过物流”、以及任务相关的关键信息“订单号是12345”。判断标准是这条信息在未来对话中是否可能被用到。怎么存用向量库做语义检索是最常见的方案。把每条记忆转成embedding存进chromadb检索时用当前问题去查最相似的几条。但这里有个坑纯语义检索会漏掉时间敏感的信息。比如用户说“我明天要出差”三天后这条记忆就没用了但语义检索还是会把它翻出来。解决办法是给记忆加时间戳和过期标记检索时过滤掉过期的。怎么取不是每次对话都检索那样太慢也没必要。我的做法是先用一个轻量判断决定“当前问题是否需要查长期记忆”需要才去检索。这个判断可以用规则比如问题里出现“上次”“之前”“我的”等词也可以用小模型做分类。检索回来的记忆不要直接塞进prompt而是作为“背景信息”放在系统提示的末尾并标注“以下信息来自历史对话仅供参考”。4.3 记忆冲突当用户改口了怎么办这是面经里很少提但实际很常见的问题。用户上次说“我喜欢红色”这次说“我讨厌红色”两条记忆冲突了。如果都存着检索时可能同时返回模型就懵了。我的处理方式是新记忆覆盖旧记忆。存新记忆时先检索是否有语义相似的旧记忆如果有且内容矛盾就把旧的标记为失效。判断矛盾可以用一个简单的规则如果两条记忆的embedding相似度高于阈值但内容情感极性相反比如一个“喜欢”一个“讨厌”就判定为冲突。这个逻辑不完美但能解决大部分情况。另一个思路是给记忆加权重新记忆权重高检索时按权重排序模型看到的是权重最高的那条。这样即使旧记忆没删也不会干扰。5. 状态管理与多轮对话并发场景下的会话隔离5.1 会话ID一切状态的锚点多轮对话的核心是“记住这是同一个人在同一次对话里”。实现方式很简单给每个会话分配一个唯一ID所有状态都挂在这个ID下。会话ID怎么生成用UUID就行客户端首次请求时生成后续请求都带上。服务端收到请求后用会话ID去查对应的状态存储。状态存储初期可以用内存字典但内存重启就丢所以生产环境要用Redis或者数据库。这里有个容易忽略的点会话ID不要用用户ID代替。一个用户可能同时开多个对话窗口用用户ID会导致状态串号。会话ID和用户ID是多对一的关系一个用户可以有多个会话。5.2 状态存什么区分“对话状态”和“业务状态”状态分两种。对话状态是agent自己的比如当前在哪个流程节点、已经收集了哪些信息、下一步该问什么。业务状态是跟具体任务相关的比如订单处理到哪一步了、支付是否完成。我习惯把两者分开存。对话状态用简单的键值对业务状态用结构化的数据模型。分开的好处是对话状态可以随对话结束而清理业务状态需要持久化保留。举个例子一个订餐agent。对话状态可能是{step: collecting_address, collected: {dish: 宫保鸡丁}}业务状态可能是{order_id: 123, status: pending_payment}。对话结束后对话状态可以删但订单状态要留着。5.3 并发请求同一个会话同时来两条消息怎么办这是面经里的进阶问题也是实际项目里一定会遇到的。用户在手机和电脑上同时发消息两个请求同时到达如果都去读同一个状态、改同一个状态就会互相覆盖。解决方案有三种。最简单的是加锁处理某个会话的请求时先获取该会话的锁处理完释放。这样同一会话的请求串行执行不会冲突。缺点是如果处理时间长后面的请求要等。第二种是队列每个会话一个消息队列请求进来先入队后台worker按顺序处理。这个方案更优雅但实现复杂一些。第三种是乐观锁读状态时记下版本号写的时候检查版本号是否变了变了就重试。适合冲突不频繁的场景。我一般用第一种简单可靠。锁的粒度就是会话ID用Redis的SETNX实现设置一个合理的过期时间防止死锁。6. 容错与降级工具挂了、模型抽风了怎么办6.1 超时与重试边界在哪里工具调用超时是常态尤其是调外部API。设置超时时间的原则是比工具的正常响应时间略长但不超过用户能忍受的极限。一般API设5到10秒慢一点的设30秒。重试策略要谨慎。不是所有失败都值得重试。网络超时可以重试参数错误重试也没用。我一般只对超时和5xx错误重试重试次数不超过2次且每次重试前等一小段时间指数退避。重试超过次数还失败就走降级。6.2 降级方案没有工具可用时agent还能做什么降级的意思是工具用不了agent不能直接摆烂得给用户一个交代。降级方案分几层第一层换工具。比如主搜索工具挂了切到备用搜索。这要求你在注册工具时就配好备选。第二层用模型自身知识兜底。告诉模型“搜索工具暂时不可用请基于你的知识回答并告知用户信息可能不是最新的”。第三层直接告知用户。如果连模型都处理不了就返回一个友好的错误提示并建议用户稍后再试。我见过最差的处理是直接把异常抛给用户显示一堆堆栈信息。这不仅体验差还可能泄露内部信息。6.3 模型输出格式错误解析失败的自修复模型有时候不按格式输出比如你要求JSON它给你一段带解释的文字。这时候直接解析会失败。我的做法是写一个宽松解析器先尝试直接解析失败后用正则提取JSON部分再失败就返回一个默认结构并把原始输出记录到日志。同时把解析失败的信息反馈给模型让它重新生成。通常重试一次就能成功。还有一种情况是模型输出了不存在的工具名。这时候不要直接报错而是把可用工具列表再给模型看一遍让它重新选。这个自修复循环最多跑两次两次还不行就放弃走降级。7. 从单agent到多agent什么时候该拆怎么拆7.1 拆分的信号单agent什么时候不够用单agent能搞定的事不要拆多agent。拆分会带来通信开销、状态同步、调试复杂度。我判断的标准是当单agent的工具列表超过15个或者系统提示超过2000字或者不同任务的上下文差异很大时才考虑拆。比如一个客服agent既要处理退换货又要处理技术咨询还要处理投诉。这三类任务的话术、工具、流程都不一样塞在一个agent里系统提示会非常臃肿模型也容易混淆。这时候拆成三个专职agent各管一摊反而更清晰。7.2 编排模式路由、流水线、还是协作多agent的编排有三种常见模式。路由模式一个调度agent根据用户意图把请求分发给对应的专职agent。适合任务边界清晰的场景。实现简单就是分类加转发。流水线模式多个agent按顺序处理前一个的输出是后一个的输入。适合有明确阶段的任务比如“信息收集→方案生成→审核”。协作模式多个agent互相通信共同完成一个任务。适合复杂问题但实现和调试最难。我一般不建议新手碰这个。我自己的项目里路由模式用得最多。调度agent只做一件事判断用户意图然后转发。专职agent各自独立互不干扰。这样每个agent的系统提示可以写得很聚焦工具列表也很短模型表现更稳定。7.3 agent间通信消息格式与状态传递多agent之间怎么传消息我的做法是定义一个统一的消息结构{ from: agent_name, to: agent_name, type: request/response/event, content: 具体内容, context: {session_id: ..., state: {...}} }context里带上会话ID和必要的状态这样接收方agent能知道当前上下文。但注意不要传太多只传接收方需要的。传多了既浪费token又可能干扰接收方的判断。状态传递的原则是谁产生谁负责谁需要谁获取。不要把所有状态都广播给所有agent那样状态同步会变成噩梦。8. 实测中的那些坑面经不会告诉你的细节8.1 工具描述里的“隐藏指令”这是一个很隐蔽的坑。如果你在工具描述里写了“当用户询问X时使用此工具”模型可能会把这句话当成指令在用户没问X的时候也去调用。解决办法是把描述写成客观陈述不要用“当...时使用”这种条件句式而是写“此工具用于查询X”。模型自己会判断什么时候用。8.2 上下文窗口的“隐形消耗”你以为上下文窗口只算对话历史不是的。系统提示、工具描述、记忆检索结果、工具返回结果全都占token。我见过一个项目工具描述写了3000字系统提示2000字还没开始对话就占了5000 token。所以工具描述要精简系统提示要克制记忆检索结果要限制条数。8.3 模型对“否定指令”的抵抗力你在系统提示里写“不要编造信息”模型该编还是编。否定指令的效果远不如肯定指令。更好的写法是“如果信息不足请明确告知用户你无法回答”。用正面引导代替负面禁止效果会好很多。8.4 日志出问题时你唯一的依靠agent开发最痛苦的是“不知道为什么模型选了那个工具”。这时候日志就是救命稻草。我建议记录这几样每次模型调用的完整输入输出、工具调用的参数和结果、状态变更前后的快照。日志不用多漂亮但要全。出问题时能回放整个链路定位就快很多。9. 学习路径复盘从面经到项目的闭环回头看用面经驱动学习最大的好处是目标明确。你知道每个知识点对应的是哪个实际问题学起来不盲目。我的建议是找一份面经把问题抄下来然后按我上面说的顺序一个一个实现。实现完一个就在面经上打个勾。全部打勾的时候你对agent开发的理解已经超过大多数只会调框架的人了。具体路径可以这样安排第一周搞定工具调用链路第二周加记忆第三周加状态管理第四周做容错和降级第五周尝试多agent。每周结束的时候写一篇简短的复盘记录踩了什么坑、怎么解决的。这些复盘就是你自己的“面经”比任何别人的面经都有价值。最后分享一个我自己的习惯每学一个新东西我都会问自己“如果面试官问我这个我会怎么答”。如果答不上来说明我没真懂。这个自问自答的过程比被动看资料有效得多。agent开发这个领域变化快但底层的那些问题——工具怎么选、记忆怎么管、状态怎么保、错误怎么处理——是不变的。把这些不变的东西吃透上面再怎么变你都能接得住。