
最近在业余时间做了一个叫《码上面试》的Agent项目简单说它是一个面向程序员求职场景的模拟面试助手。市面上类似的面试刷题工具太多了但大多数都是“题库固定题解”的模式候选人对着标准答案背实际面试时遇到追问就露馅。我想要的不是一个题库而是一个会主动提问、会追问、会评估的AI面试官。这就决定了它必须是一个Agent项目而不只是套壳聊天机器人。这种需求天然适合Agent架构它要感知候选人上一轮的回答、调用题库和知识库工具、决定追问方向还要在对话中维护候选人画像。这篇是这个系列学习记录的第一篇重点梳理我搭起第一个多Agent流程时踩过的坑和沉淀下来的思路主要想聊三个问题为什么这样设计角色、手写ReAct循环时遇到哪些边界问题、多Agent协作时怎么管好评分与记忆。如果你也在折腾agent开发、ai agent搭建或者刚看完吴恩达那种Agent教程不知道怎么落地这篇应该能提供一些可复用的经验。1. 项目整体拆解为什么先做“面试官”而不是“做题家”1.1 面试场景的真实痛点做这个项目之前我访谈过几个正在跳槽的朋友也回看了自己当年准备面试时的记录。本质痛点其实就三个第一个是“题海战术效率太低”市面上的题库动辄几千道但候选人很难知道哪些题对自己的目标岗位、自己的薄弱点有针对性第二个是“追问缺失”背会了一道题的标准答案面试官换个角度深挖一层立刻露馅因为静态题解完全不含追问逻辑第三个是“复盘主观”面完感觉良好但不知道到底哪里好、哪里差没有结构化的能力评估。这三个痛点单靠传统的问答机器人是解不了的。传统ChatBot更像一个“答案检索器”你问它红黑树的性质它给你一段标准定义然后对话就结束了。而面试场景真正需要的是一连串行为根据简历信息判断候选人应该被问简单还是难的问题根据上一轮回答质量动态调整追问方向如果候选人的描述里涉及某个框架实时查一下这个框架的最新版本特性再继续追问最后还要对整个面试过程打分并生成复盘报告。这一串行为拆开来看每一步都不复杂但连起来需要系统具备“感知-决策-行动-观察”的闭环能力这正是Agent的核心循环。所以我才决定用Agent架构来做而不是堆一个大prompt的机器人。我还发现一个容易被忽略的点候选人技术能力通常分好几项比如算法、操作系统、网络、项目深挖、系统设计。传统题库往往一次只考查一个方向但真实面试是混着来的。Agent的价值在于它可以维护一个候选人能力画像在回答中随时识别候选人的短板然后在后续轮次里针对短板出题。这个能力需要跨多轮对话的状态管理天然属于Agent擅长的领域。1.2 从单Agent到多Agent的选型思考一开始我确实试过单Agent方案一个模型实例同时扮演面试官、评分员、知识库检索员。结果很快发现不可行。原因在于面试官需要保持对话的压迫感和快节奏评分员需要冷静梳理打分维度知识库检索员只要忠实地查资料这三个任务对语气、上下文窗口、输出格式的要求完全不一样。硬塞进一个对话上下文里模型的行为会不自觉地漂移有时候它突然开始长篇大论地背知识点不像在面试有时候它又忘了自己还在等候选人回答直接替候选人把答案说了。后来我做了个简单的实验来验证这个问题用同一个Prompt模板分别跑单Agent和多Agent在20轮模拟面试里统计“角色错乱次数”。单Agent在12轮之后出现明显的角色遗忘多Agent几乎没出现过。当然这个实验不够严谨但对我的决策足够有说服力。多Agent的本质是用工程复杂度换“上下文纯净度”每个Agent只在自己的职责范围内思考看到的信息被裁剪到最小必要集合语气和输出格式各自约束最终结果用代码逻辑串联。不过这并不意味着我要直接从重框架起步。我选择先用一段自己手写的代码程序把这些Agent串起来而不是立刻套LangChain、LangGraph或者某个agent框架。原因有三点第一我想通过手写过程理解Agent循环的底层机制比如模型输出里的Thought/Action怎么被解析、工具结果怎么回填、上下文怎么增量维护第二面试场景的流程在初期变化很快手写代码改起来更灵活框架的抽象反而碍手碍脚第三调试成本。前期跑不通的问题大多数是Prompt问题、格式解析问题和状态丢失问题手写能让我一眼定位到出错位置用框架反而多一层黑盒。当然我也清楚手写方式的天花板当Agent数量超过三个消息路由、状态持久化、并发控制都会变得繁琐。所以我在项目里留了一个替换层后续如果协作复杂度继续上升就迁移到LangGraph之类的编排框架。最后我的架构定成了四个角色面试官Agent、知识库检索Agent、评估Agent、复盘汇总Agent这四者通过一个简单的消息队列通信项目里管它叫面试调度核心。2. 第一个可运行的Agent流程从提示词到ReAct循环2.1 最小闭环让模型先学会“按流程说话”我最开始做的并不是华丽的功能而是先跑通一个最小可用的ReAct循环。ReAct这个模式现在讲agent开发基本绕不开它的核心是让模型交替输出Thought、Action和Observation形成一个“思考-行动-观察”的闭环。用一个生活化的类比你中午点外卖先想“今天想吃辣的还是清淡的”这是Thought然后打开外卖App搜索“川菜”这是Action看到一堆候选店铺这是Observation看完评价后决定点哪家这又触发下一个Thought。Agent做事的逻辑也一样只不过它的“App”是一个个工具。我在项目里的第一个最小闭环是这样的面试官Agent收到用户说“我想面后端岗位”之后先思考这道题应该出什么难度然后调用一个叫做出题器的工具工具返回几道候选题目Agent再把这些题目整理成自然语言发出来。这个流程听起来简单但落地时有个关键细节模型输出的Action必须能被程序解析。我最初让模型输出自由格式的JSON结果解析时经常翻车。后来我改成一种更稳的格式让模型先输出一行ACTION:结尾的文本再换行输出JSON内容程序用正则按标记位截取。我写这个最小循环时用的Python代码大概是这样的给新手一个参考def run_agent_loop(user_input, tools, max_steps5): messages [{role: system, content: system_prompt}, {role: user, content: user_input}] step 0 while step max_steps: response llm.chat(messages) action extract_action(response) if action[type] finish: return action[output] if action[type] call_tool: result execute_tool(action[tool_name], action[arguments]) messages.append({role: assistant, content: response}) messages.append({role: tool, content: f工具返回{result}}) step 1 return 达到最大步数自动结束这个循环是所有Agent项目的骨架后面做的所有复杂功能都是在往这个循环里塞更精细的Prompt和更多的工具。注意我没有直接把工具结果拼进用户消息而是单独标记一条role: tool的消息这点很重要。很多模型在微调时已经见过工具消息的格式这样做能让模型更好地理解哪些内容是工具返回的而不是把工具结果误当成用户说的话。2.2 用代码控制推荐逻辑而不是把一切丢给模型跑通最小循环之后我犯了一个很多agent初学者都会犯的错试图让模型自己决定所有事情包括题目难度选择。我在面试官Agent的Prompt里写了一句“请根据候选人的水平出合适的题目”然后模型就开始自由发挥有时候出一个中等难度题有时候直接出hard题完全没有标准。我意识到这个问题后专门花了一晚上梳理“难度控制”的决策逻辑最终结论是难度这类可以结构化的决策应该交给代码规则而不是交给模型感觉。具体我做了这么一件事第一轮先让候选人给自己打三个分分别是算法能力、工程能力、项目经验每项1到5分。然后面试官Agent只是把这些分数读进上下文真正决定第一题难度的逻辑是代码里的规则引擎三项总分大于等于12分就给hard题9到11分给medium题9分以下给easy题。等候选人答完第一题评估Agent会返回一个能力值变化量这个变化量再回流到难度调节逻辑里。模型在中间扮演的角色并不是“决定难度”而是“根据给定难度生成符合面试习惯的题目表述”。换句话说模型负责的是语言生成代码负责的是决策控制。这个取舍我觉得是Agent项目里最关键的原则之一能放进代码的确定性绝不要放给模型自由发挥。模型擅长的是开放式的语言理解与生成不擅长的是精确的条件判断和数学规则。把不带确定性的任务比如“判断候选人评分是否达到6分”交给模型等于把系统的稳定性交给概率这在实际工程里是完全不可接受的。2.3 为什么我暂时没有上复杂框架很多朋友看到Agent项目第一反应是去选框架比如LangGraph、Dify、Coze或者其他agent平台。我之前也反复比较过最后在项目第一阶段选择暂时不上框架几千行代码跑完才发现这个决定帮我把基本功打扎实了。框架能帮我们解决编排、状态管理、工具注册这些通用问题但前提是你得先理解这些问题为什么存在。手写阶段让我直面了三个框架抽象掉的关键点模型输出格式的脆弱性、多轮上下文的状态维护、Agent之间的通信协议设计。第一阶段结束后我整理了一个权衡表格给后面选型用维度手写轻量实现成熟框架如LangGraph学习成本高但收益直接中先会调用再理解底层调试体验非常直观完全可控偶有黑盒需要查文档状态管理自己维护简单但原始内置持久化和检查点多Agent编排需要手写队列内置节点图和条件路由后续扩展需要自己造轮子生态丰富插件齐全我的结论是前期适合手写中期适合框架后期可能还需要自己改框架。这篇学习记录碰巧在“手写但准备迁移”的时间点上所以公众号里面很多细节是手写实现但思路参考了LangGraph的状态机模型。我计划在第二阶段引入更完整的记忆系统和MCP工具协议届时再评估是否需要迁移。3. 多Agent协作面试官、评估者、知识库检索者如何分工3.1 Agent角色的划分与提示词设计多Agent协作的第一步是分清楚角色边界。我在项目里把Agent按职责切成了四块面试官Agent负责所有面向用户的提问语言知识库检索Agent负责在题库和技术文档库中检索参考信息评估Agent负责对候选人的回答做结构化评分复盘汇总Agent负责在面试结束时把多维度的评估结果整理成一份报告。这里最核心的设计原则是“谁面向用户谁负责表达”知识库检索Agent哪怕查到了再好的内容也不能直接输出给候选人必须返回给面试官Agent由面试官用自己的语气转述。提示词设计方面我有一个很大的经验转变。最开始我把每个Agent都写成了“人设”比如给面试官Agent写“你是一位经验丰富的技术面试官对待候选人亲切友善善于引导”看起来没什么问题但实际跑下来效果很虚。后来我把Prompt彻底改成“职责说明书”式的写法重点放在三个部分输入说明、任务边界、输出格式。给一个比较典型的Prompt示例你的角色是面试官Agent。 输入候选人的简历信息、上一轮回答的评估摘要、本轮知识库检索结果。 职责基于输入内容生成一个自然的面试追问问题。不要替候选人回答。 约束问题长度不超过150字每次只问一个问题不要自带答案。 输出格式JSON包含字段type固定为question和content问题文本。这种Prompt风格的效果好得多因为任务边界清晰模型不会因为被赋予“性格”而产生幻觉。我后来还发现一个经验在输出格式里明确“不允许出现的内容”比只规定“允许出现的内容”更有效比如明确写“不要自带答案”能显著减少模型抢答的现象。3.2 上下文窗口与记忆管理的折中方案多Agent跑起来之后内存占用问题立刻暴露了。最开始的实现很粗糙每个Agent收到的是全量对话历史相当于面试官、评估者、知识库检索者都在重复读同样的几十轮对话。结果有20轮面试跑到一半上下文长度突破模型窗口限制直接报错。我后来用了一个折中方案核心是“最小必要上下文”原则每个Agent只收到它完成任务所需的最少信息其余信息一律不传。具体实现上我维护了一个面试状态对象里面只包含结构化字段比如候选人基础信息、当前题目难度、最近三轮问题的摘要、候选人对每类知识点的掌握度、上轮评分等。面试官Agent发消息时程序把候选人的回答先转成一句由摘要模型生成的短摘要再把这份摘要连同当前题目信息传给面试官。知识库检索Agent则只收到查询关键词检索出结果后以工具消息的形式返回给调度核心。评估Agent收到的信息最完整但也只是当前轮次的问答全文加历史评分表格不会收到前几轮的完整对话。记忆管理方面我区分了短期记忆和工作记忆。短期记忆就是当前面试会话内的对话缓存因为面试通常控制在40分钟以内缓存不会爆炸。工作记忆则是跨场次持久化的比如候选人上一次面试时在哪些知识点上表现弱下次面试开始时可以直接拉出来用于追问。这个我用一个简单的SQLite表来存表里记候选人ID、知识点、掌握度打分和时间戳。这样设计下来哪怕大模型只有有限的上下文窗口Agent也能通过“滚动摘要结构化状态”保持对长流程的控制。3.3 路由与状态传递消息总线模式多Agent之间的通信我一开始是直接函数调用A调B、B调C代码耦合到后来自己都看不懂。后来我改成一个轻量的事件总线模式调度核心内部维护一个消息队列每个Agent完成自己的任务后往队列里扔一条消息。消息类型有面试官提问事件、候选人回答事件、检索请求事件、检索结果事件、评分完成事件。调度核心根据事件类型和当前状态决定下一步该触发哪个Agent。这套消息总线模式的核心优势是解耦。每个Agent不需要知道其他Agent的存在只需要知道自己收到指令后该做什么。面试官Agent不会直接调用评估Agent它只是发出一条“候选人回答完成”的事件调度核心再决定把这个事件转交给评估Agent。这样新增Agent时不需要改既有Agent的代码只需要在调度核心注册新的事件类型就行。比如我后来加了“追问策略Agent”就是通过事件路由接进来的老Agent完全无感。对于只有三五个Agent的项目来说这个模式比上消息中间件要轻量得多也足够应付。这里有一个容易踩的坑Agent之间消息传递如果走自然语言文本模型可能会误解消息内容。所以我规定消息体必须包含结构化字段比如{event_name: candidate_answer, candidate_id: 1001, round: 12}。自然语言文本只作为辅助内容不影响路由决策。路由决策完全靠代码读结构化字段来做不依赖模型理解这样下来稳定性高很多。4. 实操中踩过的坑与排查记录4.1 Agent把“搜索”和“回答”混在一起第一次正式模拟面试时候选人问了一道“什么是JVM内存模型”结果面试官Agent调用了知识库检索工具拿到资料后没有任何加工直接把工具返回的整段文档原文给候选人读了一遍。这段回答风格又硬又长完全没有面试官该有的语气候选人体验非常差。我当时以为是Prompt写得不够好加了很多描述“请用口语化的方式回答”效果仍然不稳定。后来我才意识到问题的本质不在语气而在职责边界。面试官Agent不应该同时承担“内容提供者”的身份它的职责是“基于已有内容进行表达”。所以我新增了一条处理规则如果面试官Agent要回答某个知识性问题它只能引用知识库检索Agent返回内容里“结论和关键词”部分并且每条引用最多提炼成三句话。知识库检索Agent的Prompt里也加了一条约束返回内容必须以知识点关键词核心结论来源参考的格式组织不允许输出完整长文。这一改之后回答质量明显回升。这个坑给我的教训是工具调用链路中信息传递的“格式设计”比内容本身更重要。如果检索结果直接原封不动拼进模型上下文模型很容易误以为可以直接把这段内容当成最终回答。而当你把工具结果设计成“中间结论摘要”的形态模型就能自然地把工具当参考而不是当答案。4.2 模型输出JSON不稳定怎么兜底评估Agent需要输出结构化评分我一开始要求模型输出严格的JSON像{algorithm: 7, communication: 8}这样。实测下来10次输出里至少有2次是坏的要么多个逗号要么JSON里混进中文说明要么外层多包了一层Markdown代码块。对于一个要上线的项目这种概率完全不可接受。我最后做了三层兜底。第一层是让模型把JSON放在Markdown代码块里这样程序可以先按代码块标记切出纯文本第二层是用json.loads解析如果失败再尝试用正则提取所有数字字段第三层是终极兜底——如果前两层都失败就让评估Agent改成输出一段自然语言评分描述然后由一个小规则引擎从描述中抽出分数关键词。实际效果是第一层就让成功率到70%加入正则后到95%最后那5%虽然还偶尔出现但已经不会让整个流程崩溃了。给一段我自己写的解析兜底函数片段你拿去就能用import json, re def robust_parse_llm_json(raw_text): # 第一层剥离 Markdown 代码块 pattern r(?:json)?\s*([\s\S]*?)\s* m re.search(pattern, raw_text) candidate m.group(1) if m else raw_text try: return json.loads(candidate) except Exception: pass # 第二层正则提取所有数字 nums re.findall(r[-]?\d\.?\d*, candidate) return {manual_score_hit: nums}这段代码虽然简单但对稳定性帮助非常大。我的经验是不要迷信大模型的输出结构化能力模型输出本质是概率采样任何格式都可能有偏差。真正负责任的做法是在模型外层包一层Schema校验和纠错逻辑把模型的“无限自由”关进应用层的“有限笼子”。4.3 长对话后上下文被污染如何做记忆压缩项目跑到第20轮时出现了一个奇怪的现象候选人前面答错的一个知识点被后续问题覆盖之后评估Agent居然完全忘了评分直接给了满分。我排查后发现问题出在上下文截断为了适配窗口限制我把超过长度限制的历史对话粗暴丢了模型自然就“失忆”了。这种粗暴截断的危害比想象中更大它会静默地丢失关键信息而你无法第一时间发现。这个问题我改成了两套机制并行。第一套是滚动摘要机制每5轮面试结束后调度核心调用一次摘要Agent把最近5轮的对话压成一段150字以内的摘要替换掉原始对话文本。第二套是关键点缓存机制面试每轮产生的评估结果都会以一种结构化的形式写入全局状态比如弱项:哈希表冲突处理索引: 89%。这样即使对话摘要丢了一些细节关键的结构化信息依然保留评估Agent在给后续轮次打分时可以参照历史关键点。两套机制并行后整个面试流程可以稳定跑完40轮而不超过上下文窗口。压缩前后的对比我简单测过同一场面试原来的上下文占用约1.8万token压缩后降到了6000 token左右而评分的连贯性反而更好。原因很好理解结构化关键点比冗长对话更清晰模型注意力不会被无关细节分散。这也是Agent记忆设计的一个核心思路记忆不是“把能存的全存下来”而是“把该记的按结构存下来”。5. 从这次学习记录里沉淀的三条经验5.1 优先把流程跑通再优化模型第一阶段我最大的收获是确立了“先流程后模型”的开发顺序。很多Agent项目失败不是因为模型能力不够而是流程还没跑通就开始追求单点效果。我的做法是先把最基本的“1轮面试”完整跑通候选人答完题评估Agent打分面试官Agent根据分数追问。这个最小闭环跑通之后后面的多Agent扩展就像搭积木一样一个一个往流程里加。如果一开始就想把所有功能一次做完你会发现自己被无数个并行问题困住根本不知道先修哪个。5.2 能放进代码的确定性绝不放给模型自由发挥这条经验几乎贯穿了项目的所有模块。从题目难度控制、事件路由、JSON解析兜底到记忆压缩策略凡是能用代码明确判断的逻辑我都没有交给模型决策。模型适合做的是开放式语言任务比如把一道题改成符合人话的提问、把候选人的回答总结成面试反馈。把这两者分开之后系统稳定性和开发心情都明显提升。任何一次你发现模型输出出现随机波动都该想一想是不是把某个确定性结口漏给了模型。5.3 下一阶段的规划接入MCP与更完善的记忆系统第一版跑通之后我已经在规划第二阶段的两个方向。第一个是接入MCP协议把题库和面经文档以标准工具协议的方式挂载进来这样知识库检索Agent可以动态发现工具不用每次新增数据源都改代码。第二个是更完善的长期记忆系统我目前用的是SQLite存面试状态后续计划引入向量数据库对候选人的历史大面试记录做语义检索让系统能更智能地基于历史弱项出题。这些内容我会在下一期学习记录里继续写。如果你也在做agent项目尤其也是面试或者教育场景欢迎带着你的问题来交流代码和踩坑笔记我都可以整理出来一起讨论。