美团 AI 应用开发一面:Agent 平台架构与 ReAct 编排
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 你实习做的 Agent 平台,整体架构是怎样的?用户怎么和你这个 Agent 交互?是 API 调用还是 IM 接入?
- 你们这个 Agent 在回答之前会做澄清反问吗?什么情况下会触发澄清?
- 整个问答链路是什么样的?从用户输入到最终输出,中间经过了哪些处理环节?
- 你们怎么做意图识别的?意图识别树是什么?怎么构建的?
- ReAct 和 Plan-and-Execute 两种模式有什么区别?各自适用什么场景?你的项目选的哪种?为什么?
- Plan 模式如果计划定死了,中途出意外怎么办?Replan 怎么设计?只重规划受影响的后半段还是全盘推翻?怎么避免计划抖动?
- Max Replans 上限怎么设?
- 如果规划用大模型、执行用小模型,怎么保证执行质量?
- 工具调用超时怎么处理?重试策略是什么?重试会不会导致重复操作?幂等怎么设计?
- 如果工具返回格式不合法,怎么解析?
- 如果模型决定调用工具,工具调用信息返回在哪里?是普通文本还是结构化输出?如果是结构化输出,具体体现在哪里?模型返回的文本内容和工具调用信息是什么关系?
- RAG 检索召回不相关怎么排查?Query 改写怎么做?这样做有什么好处?
- 混合检索方案怎么实现?BM25 和向量检索结果怎么融合?RRF 的 k 值怎么设?
- 如果用户搜”数据权限”,但代码里只有 RBAC,怎么检索到?如果两个概念不是语义相似,只是逻辑相关,比如”维C”和”水果”,向量检索怎么办?
- 你平时怎么用 AI 写代码?你的 AI Coding 工作流完整描述一下。从拿到需求到提交代码,AI 参与了哪些环节?每个环节你做了什么?
- 你如何确保 AI 生成的代码质量?如果 AI 生成的代码能跑但逻辑很烂,哪些问题必须改?你怎么发现这些问题?有没有自动化检查手段?
- 如果 AI 连续三次没修好一个 Bug,你会怎么处理?有没有遇到过真实案例?
- 手撕:岛屿数量。给定由
'1'(陆地)和'0'(水)组成的二维网格,计算岛屿数量,要求分别用 DFS 和 BFS 实现。追问:网格特别大(如 10000x10000)递归会栈溢出,怎么改成 BFS?用并查集怎么做?时间复杂度分别是多少? - 自我介绍,重点讲你实习做的 Agent 平台。
《参考解析》
ReAct 与 Plan-and-Execute 的取舍,以及 Replan 怎么做
ReAct 是”想一步、做一步、看结果再想下一步”,每一步都用最新观测重新决策,适应性强、对工具报错和意图漂移容忍度高,代价是每步都要一次 LLM 调用、容易被局部观测带偏、长任务上容易兜圈子。Plan-and-Execute 是先用大模型产出一份结构化计划(步骤列表 / DAG),再由执行器逐步执行,好处是可以把规划和执行拆给不同模型,执行步骤能并行、能缓存、可审计,代价是计划一旦与现实不符就会僵化。选型看两个轴:任务路径是否可预判、出错代价是否高。路径清晰、步骤可枚举(数据同步、批量报表、多步检索)用 Plan;需求模糊、需要边探索边收敛(排障、开放式研究)用 ReAct。工程上常见的是混合式:先给一份粗计划定方向,每一步内部用 ReAct 做局部决策——即”外 Plan 内 ReAct”。
Replan 的设计要点:① 触发条件要明确——某步连续失败 N 次、工具返回与预期 schema 不符、观测到前置假设被推翻(比如”库里没有这张表”),都触发重规划,而不是每步都重规划;② 重规划范围默认只覆盖”受影响的后半段”(计划里该步骤之后、且依赖它的那些节点,即 DAG 上的可达子图),全盘推翻只在目标本身变了时才做,否则前面花掉的 token 全浪费还容易引入新错;③ 避免抖动靠三件事——把已完成步骤的产物作为不可变事实喂给重规划 prompt、给重规划设次数上限(通常 23 次就够,超过说明任务本身不可行或模型能力不够,应该降级或转人工)、以及让重规划输出必须与既有计划做 diff(只允许新增/修改未执行节点,禁止改已执行节点)。Max Replans 的合理取值要看任务长度:短任务 12 次,长任务 3~5 次,同时配合总步数和总 token 的双重硬上限,别让”重规划”变成死循环的新出口。
意图识别与澄清反问
意图识别树本质是一棵分层的分类结构:根节点按领域粗分(查询类 / 操作类 / 闲聊类 / 兜底),往下按业务对象细分(订单、账号、权限、报表),叶子节点对应可执行的动作和需要的槽位。构建方式通常是三层配合——规则/关键词兜住高频且必须精准的意图,小模型分类器(或 embedding 近邻)处理长尾说法,大模型只在不置信时兜底判一次,这样比每个请求都上大模型便宜得多,也稳定得多。树的价值在于每一层都能挂不同的澄清策略和槽位约束,而不是只为了分类。
澄清反问的触发条件要收敛到三类:槽位缺失且该槽位无法从上下文或用户档案推断(不能瞎猜,尤其是金额、时间、对象这类会造成副作用的参数);意图置信度低且候选意图的处置方式差异大(差到执行错了要赔钱,就一定要问);动作有不可逆副作用(删除、支付、对外发送)。反过来,能从上下文补齐、或所有候选意图处置相同的情况不该问,否则用户会觉得助手啰嗦。整条问答链路的通用骨架是:输入预处理(敏感词/注入检测、指代消解)→ 意图与槽位识别 → 澄清(如需)→ 检索或规划 → 工具执行(带校验与重试)→ 结果综合与生成 → 安全检查与格式化输出,每一环都要有埋点,否则线上出问题无法定位是识别错了还是执行错了。
工具调用:超时、重试、幂等与结构化返回
工具调用必须带三项配置:连接/读取超时(例如 3s/10s)、重试策略、幂等键。只对可重试错误重试——超时、429、5xx、连接重置;参数错误、鉴权失败、404 这类 4xx 重试毫无意义只会放大延迟。退避用指数 + 抖动(0.5s → 1s → 2s,上限 1030s,加 ±20% 抖动避免惊群),重试 23 次,并且整条链路要设重试总预算,防止多层各自重试叠乘。
重试会重复执行,所以写操作必须有幂等保证,三种可落地的做法:工具服务端接受 idempotency_key(由调用方按”任务 id + 步骤 id + 参数哈希”生成)并在存储层对 key 建唯一索引,重复请求直接返回首次结果;或者把”先查后写”做成乐观并发(带版本号 CAS);再或者把副作用变成幂等操作本身(用 PUT/upsert 而不是 append)。读操作天然幂等,重试无风险。
工具返回格式不合法时先修后弃:优先让工具侧定义严格的输出 schema(JSON Schema/Pydantic 校验),调用方按层级解析;解析失败把原始返回截断后连同校验错误回填给模型做一次修复重试;再失败就返回”工具不可用”这个结构化观测给 Agent,让它换工具或降级回答,而不是让异常直接冒泡成 500。
模型决定调用工具时,工具调用信息不在文本里,而在结构化字段里:OpenAI 兼容协议里它是 choices[0].message.tool_calls(含 id、function.name、function.arguments 的 JSON 字符串),Anthropic 协议里是 content 数组中 type: "tool_use" 的 block(含 id、name、input)。文本内容和工具调用信息的关系是”互斥或并存”:模型要么输出纯文本回答,要么输出一个/多个工具调用;开启并行工具调用时两者可以同时出现(先说一句”我来查一下”再并发调多个工具)。执行完工具后,结果要以 role: "tool"(或 tool_result block)并按对应的 tool_call_id 回填,模型才能把结果和之前的调用关联起来——这个 id 关联是整条链路最容易写错的地方。
混合检索与逻辑相关:BM25、向量与 RRF
召回不相关按”query → 切分 → 向量 → 排序 → 生成”逐段排查(见另一篇里更细的排查流程),这里补两个具体手段。Query 改写的好处是把用户口语化的输入变成检索友好的表达:指代消解(“它”指什么)、补全省略(“维C”→“维生素C 食物来源”)、扩展同义词与缩写(“数据权限”→“数据权限 RBAC 行级权限”)、拆分子问题(多跳问题一次检索必然差)。多查询扩展(生成 3 个不同角度的查询,各自检索后融合)对召回率提升最直接,代价是检索次数翻倍、延迟上升,所以常在离线评测里确认真有收益再上。
混合检索的意义是向量和 BM25 互补:向量擅长语义近似但对精确串(报错码、函数名、编号)无力,BM25 反之。融合最常用 RRF(Reciprocal Rank Fusion):对每路结果按排名算 1/(k + rank) 累加得分重排。k 的作用是压平头部优势——k 越小头部权重越极端,k 越大各排名越平均。经验取值 60(原论文推荐值)作为默认,实际要在自己的评测集上调:候选池小、两路质量相当可以调到 1020 让头部更强势,候选多、怕单路噪声影响就用 60100。RRF 只用到排名不用分数,好处是不用做分数归一化(BM25 分和余弦相似度量纲完全不同),缺点是丢掉了分数信息,如果两路分数都可信、也可以做加权归一化融合(min-max 或 z-score 后加权)。
“用户搜数据权限、代码里只有 RBAC”这类问题,靠的是知识侧补桥而不是换检索模型:建同义词/别名表或术语库把业务词映射到实现词,建知识图谱把”数据权限 — 实现方式 → RBAC/ABAC/行级权限”这类关系显式连起来,在文档里补一段”数据权限在系统里的落地方式”(把术语写进正文,让 BM25 能匹配到),或者用 Query 改写让模型补出同义实现词。同理,“维C 和水果”不是语义相似而是逻辑相关(属于/包含关系),向量检索天然抓不到这种上位词关系,解法是:把商品/概念的知识图谱作为独立检索通道(沿 属于、是…的一种 边扩展查询),或者对 embedding 做微调(用”上位词—下位词”对做对比学习),最实用的是离线把这类关系固化成”查询扩展表”,检索时先扩展再召回。别指望通用 embedding 理解分类学关系。
AI Coding 工作流与代码质量把关
一个可复述的完整工作流是:需求 → 我读懂并写出验收标准与边界条件 → 让 AI 先做方案设计(列改动点、接口、风险,我不接受直接给代码)→ 我确认方案并拆分任务 → AI 按任务生成代码 → 我逐块 review + 跑测试 → 补测试与文档。每个环节人的职责不同:需求阶段人负责对齐和识别”没写出来的约束”;设计阶段人负责判断方案是否过度设计、是否破坏既有约定;编码阶段人可以让 AI 大量产出,但每一行都要人看得懂;验证阶段人和 AI 分工,测试用例由人给出关键场景(边界、异常、并发、权限),AI 负责补全和填充。
“能跑但逻辑烂”的代码,必须改的是这几类:错误处理被吞掉(except: pass、忽略返回值)、边界条件缺失(空集合、null、越界、除零)、资源泄漏(未关闭的连接/文件/事务)、N+1 查询与明显的算法复杂度问题、写死的魔法值与配置、以及绕过既有抽象层次直接操作底层(破坏分层会导致后续不可维护)。发现手段不能只靠眼睛:静态检查(类型检查、lint、复杂度/重复度扫描)、跑测试看覆盖率增量、git diff 逐块读、把代码丢给另一个模型做对抗性 review(让它专门找反例和失败路径)、以及在 review 时追问”这段代码在什么输入下会错”。
AI 连续三次没修好同一个 Bug,正确的动作是停止喂它更多上下文,回到人这边做归因:先把最小可复现用例写出来(很多时候写用例的过程就发现问题了),确认是需求理解错了、还是模块边界/抽象错了、还是纯实现细节错了。如果是抽象错了,AI 越改越乱,必须人重构骨架再交给 AI 填;如果是信息不足(AI 不知道某个约束、看不到某个调用方),补齐上下文再让它改;最后不管哪种,都要把它前三次的修改 diff 全部回滚到一个干净基线再动手,别在它堆出来的补丁上继续打补丁。同时检查有无自动化手段能防止复发——加一条断言或回归测试,比让它保证”下次注意”有用。
手撕:岛屿数量
DFS 的思路是遇到 '1' 就把计数加一,然后从这个格子开始把相连的所有 '1' 淹没成 '0'(原地修改省 visited 数组),继续扫描。BFS 用一个队列把邻接点入队,同样是淹没标记。两者时间复杂度都是 O(m·n)(每个格子最多进队/访问一次),空间 DFS 是递归栈 O(m·n) 最坏,BFS 是队列 O(min(m,n)) 量级。
10000×10000 会栈溢出,所以要用显式栈/队列替代递归——把 DFS 的递归改成自己维护一个 list 当栈,本质上和 BFS 只差弹出方向;同时要注意 Python 里 sys.setrecursionlimit 治标不治本,默认递归深度 1000,网格长条状时深度轻松上万。原地把访问过的陆地改成 '0'(或 '2')是关键技巧,否则 visited 矩阵在 1e8 规模下光是布尔数组就要 100MB。
并查集的做法:把所有陆地格子当作节点,只向右和向下两个方向做 union(避免重复),最后统计根节点数量(或初始把每个陆地当独立集合,每次成功 union 就减一,最后剩下的集合数就是岛屿数)。时间复杂度 O(m·n·α),α 是反阿克曼函数,近似常数,实际比 BFS 稍慢但支持增量查询(动态加陆地时能快速更新)。用并查集要配按秩合并 + 路径压缩,否则退化成 O(n) 查找——这一点是面试官最容易追问的地方。