网易 Agent 开发一面:SSE 与 Streamable HTTP、MCP 版本兼容、记忆分层设计
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 你们团队现在有多少人?
- 现在能否正常来实习?
- 实习经历主要做了哪些事情?
- 项目经历里你负责的是哪部分?
- SSE 和 Streamable HTTP 的区别是什么?
- 如何适配 stdio、HTTP、SSE、Streamable HTTP 这几种传输方式?
- 如何处理 MCP 的版本兼容问题?
- ChatGPT 这类对话 Agent 的会话表和消息表字段怎么设计?
- MCP 的客户端和服务端交互完整流程是怎样的?
- MCP 获取工具列表是怎么做的?
- 如果 MCP 有很多工具,比如 100 个,Agent 怎么处理?
- Agent 怎么保证工具调用的准确性?
- Agent 的记忆模块怎么设计?
- 长期记忆和短期记忆分别怎么实现?
- 对话超过模型上下文窗口怎么处理?
- 压缩的具体方案是什么?还能怎么细化?
- 分层记忆怎么分?
- 项目里有用到 MCP 吗?MCP 解决什么问题?
- 站在测试开发角度,怎么验证功能符合预期?
- 项目里用到哪些组件?
- MQ 有哪些特性?
- Redis 的缓存穿透、击穿、雪崩分别是什么?怎么解决?
- 布隆过滤器的原理是什么?误判怎么处理?
- 数据库和 Redis 如何保持数据一致性?
- 旧缓存删除后旧数据还没更新,请求把旧数据写回缓存怎么办?
- Agent 的长期记忆和短期记忆如何设计?哪些内容作为何种记忆?
- 发生记忆冲突怎么处理?
- 用户姓名等信息被污染怎么处理?
- RAG 系统的流程是怎样的?如何提高准确率?
- 手撕:最长递增子序列。
具体项目追问
- 自动切题 Agent 中的 7 步 Pipeline 流程是什么?
- LangGraph 设计中的 3 个子图 Agent 之间怎么传递数据?
- 为什么拆成三个子图而不是一条 Pipeline?
- Skills 知识进化流程怎么搭建?
- 为什么不在 Prompt 中添加而用 Skills?
《参考解析》
- SSE 与 Streamable HTTP:SSE 是单向的服务端推送——客户端发一个 HTTP 请求建立
text/event-stream长连接,服务端只能往客户端推文本事件,客户端想发消息得另开一个 POST 请求。Streamable HTTP 是 MCP 后来引入的传输:只暴露一个 HTTP 端点,POST 的响应体既可以是单次返回的 JSON,也可以是text/event-stream流;它补上了 SSE 方案缺的东西——用Mcp-Session-Id维护会话、用Last-Event-ID支持断线续传、通过 GET 打开流让服务端主动推送,并把旧版「GET /sse 建流 + POST /messages 发消息」的双端点合并成一个。回答时把「谁只能推、谁能收发、几个端点、能不能断线恢复」这四点对齐,差异就清楚了。 - 传输方式适配与 MCP 版本兼容:适配的思路是先把「协议消息」和「传输」解耦,抽一层 transport 接口统一
send/onmessage/close,stdio 用子进程标准输入输出的按行 JSON-RPC 实现,HTTP/SSE 与 Streamable HTTP 各写一个实现,上层 Agent 代码不感知差异。版本兼容分两步:握手时客户端在initialize里带上自己支持的protocolVersion,服务端回它选定的版本与capabilities,双方按协商结果决定走哪些能力(tools / resources / prompts、是否支持列表变更通知);客户端侧做多版本支持,把版本相关分支收在 adapter 里按协商结果开关,未知字段忽略、不认识的版本明确报错而不是猜着跑。 - MCP 交互流程与工具列表:
initialize(带协议版本与客户端能力)→ 服务端回serverInfo与能力清单 → 客户端发initialized通知 → 之后按需调用tools/list、tools/call、resources/read、prompts/get。工具列表就是tools/list,返回每个工具的 name、description、inputSchema,支持cursor分页;服务端工具集变化时发notifications/tools/list_changed,客户端收到就失效本地缓存重拉。工具到 100 个以上时不能把 schema 全塞进上下文:按领域分组只加载当前任务需要的那组、用检索按 query 召回 Top-K 工具、给一组工具暴露一个聚合入口再往下钻、描述和 schema 精简到必要字段,再配上列表缓存与按需刷新。 - 工具调用准确性:描述里写清「什么时候该用、什么时候不该用、返回什么」,参数用 schema 严格约束并给示例;粒度要适中,太细模型要串很多步,太粗参数说不明白;调用前做参数校验,非法入参直接拦住并把错误作为观测回灌给模型;有副作用的工具做幂等、超时、重试上限和二次确认;工具结果结构化并控制长度;再加结构化输出、少量示例和「先规划再调用」。工程侧要有完整的调用轨迹用于归因,最后用真实任务集做回归——重点看「不该调用时是否克制」和「调用失败后能不能自己换路」。
- 会话表与消息表设计:会话表存 id、user_id、标题、系统提示与模型版本、状态、创建/更新时间、累计 token 与成本、最后一条消息 id;消息表存 id、session_id、role、内容、内容类型、tool_calls 与 tool_call_id、parent_id(支持重新生成与分支)、token 数、状态、创建时间,按
(session_id, created_at)建索引。内容很长或含附件时把正文拆出去放对象存储,消息表只留引用;再单独建工具调用记录表和长期记忆表(user_id + 记忆类型 + 向量索引)。量大以后按 session_id 做冷热分离与分片。 - 记忆模块与分层:短期记忆是当前会话的原始消息窗口,保证最近若干轮的细节不丢;中期记忆把更早的对话滚动压缩成摘要,摘要必须保留数字、人名、结论这类硬信息,不能只剩流水账;长期记忆把稳定事实(用户偏好、项目背景)写进外部存储,需要时按 query 检索召回而不是每轮全量带上。写入要过滤(低信息量和重复内容不进库),冲突要覆盖(改了口径旧记忆必须失效),召回要限额并标注来源与时效,任务清单这类状态外置成结构化数据由程序维护,比让模型从聊天记录里反推可靠。
- 上下文超限与压缩细化:常规手段是滑动窗口丢弃最老的消息、滚动摘要、把长工具结果摘要化或外置成文件只留引用。能细化的是压缩的粒度(轮级 / 话题级 / 实体级,话题切换时压缩比按 token 阈值压缩更自然)、触发时机(按 token 水位、按话题边界、按工具返回大小)、保真校验(拿几个关键约束反过来提问,检查摘要是否丢了长度限制、字段口径、指代关系),以及把稳定不变的内容固定放系统提示最前面以便复用前缀缓存。
- 缓存穿透、击穿、雪崩:穿透是查的 key 数据库里根本不存在,每次都打到库,解法是缓存空值加短 TTL、布隆过滤器前置拦截、入参校验。击穿是热点 key 过期的瞬间大量并发一起回源,解法是单飞(只放一个请求去加载,其余等待)、逻辑过期(不设 TTL 由后台异步刷新)、热点 key 常驻加主动更新。雪崩是大批 key 同时过期或 Redis 整体不可用,解法是过期时间加随机抖动、本地加 Redis 多级缓存、集群与哨兵保证高可用、限流熔断降级加缓存预热。
- 布隆过滤器:一个位数组加 k 个哈希函数,插入时把 k 个位置置 1;查询时只要有任意一位是 0 就说明一定不存在,全是 1 只能说可能存在。误判来自不同元素的哈希落到了同一批位上。降低误判的办法是加大位数组、把哈希个数调到接近最优(k ≈ (m/n)·ln2)、控制实际元素数不超过设计容量,超了就扩容重建;另外它不支持删除,需要删除时得换计数布隆过滤器或布谷鸟过滤器。
- 缓存与数据库一致性:主流是 Cache Aside——读未命中就回源并回填,写的时候先更新数据库再删除缓存(删比更新好,避免并发写入把旧值覆盖进去)。删除失败用延迟双删或订阅 binlog 异步补偿兜底,强一致场景直接加分布式锁或者读库,接受最终一致就要把不一致窗口和兜底 TTL 说清楚。旧值回写这个具体场景的根因是:读请求 A 未命中读到旧值,写请求 B 更新库并删了缓存,A 随后把旧值写回缓存,缓存就长期停在旧值上。解法是给回填加条件——值里带版本号或更新时间戳,回填前比对、旧版本不写;或者用
setnx抢占回填权、让回源加回填走同一把锁;再叠加延迟双删和短 TTL 兜底。 - RAG 流程与准确率:入库侧是解析、清洗、切分、向量化、建索引;查询侧是 query 改写与多路召回、结果融合与 rerank、拼装上下文、生成并输出引用。提高准确率要分环节定位:切分按语义和文档结构而不是定长,检索做向量加关键词的混合召回,加一层 rerank 精排,用元数据过滤缩小范围,系统提示里明确「证据不足就说不知道」,最后用带标注的问答集算召回率与端到端正确率,看问题出在召回还是生成。
- MQ 的特性:异步、解耦、削峰填谷;可靠性上靠持久化、消费确认与重试、死信队列、幂等消费;顺序性靠分区内有序来保证;此外还有广播与集群消费、延迟与定时消息、事务消息与最终一致、积压监控和消费位点管理。选型上 Kafka 偏吞吐与流式,RocketMQ 偏事务与延迟消息,RabbitMQ 偏灵活路由。
- 手撕最长递增子序列:朴素做法是
dp[i]表示以 i 结尾的最长长度,转移是往前面找比它小的最大值加一,复杂度 O(n²)。更优的是贪心加二分:维护tails[len]表示长度为 len 的递增子序列的最小结尾值,每来一个数就二分找到第一个大于等于它的位置替换掉,答案就是 tails 的长度,复杂度 O(n log n)。注意tails本身不是某个真实的子序列,只是各长度下的最优结尾,要还原具体序列得额外记前驱。 - 为什么拆子图、为什么用 Skills:拆成多个子 Agent 是为了让每个节点有独立的职责、上下文与评测口径,可以单独替换模型、单独做复用,出错时能定位到具体节点,也方便并行;一条大 Pipeline 的问题是全流程状态挤在一个上下文里,任何一步的偏差都会污染后面。子图之间传数据靠一份显式 schema 的共享 state(或事件消息),state 要可序列化以便中断恢复。把知识从 Prompt 挪进 Skills 的理由是:Prompt 只保留路由与决策,知识与长文档按需加载,既省 token 又能独立版本化和评测;知识进化的闭环就是收集失败案例、归纳成规则、写进对应 skill、再跑回归验证有没有真的变好。