前几天有个同事去面网易的 Agent 开发岗一面聊得挺顺利二面的时候面试官问了一个看起来很基础的问题“开发 Agent 的时候LLM 返回的是流式响应你应该用什么方式逐 token 处理输出”他想都没想就答了“用 stream 接口嘛for 循环遍历 text_stream每个 token 到了就处理。”面试官点点头又问了一句“那工具调用的参数也是流式返回的你也是每个 token 到了就 parse 吗”他愣了一下说“工具调用……不也是文本吗应该差不多吧”面试官没有直接说对不对只是又问了一个问题“那如果 JSON 只来了一半你 parse 会怎样”他挠了挠头说会报错。面试官接着问“那你怎么办等它来完怎么判断它来完了”他突然意识到自己从来没认真想过这个问题——stream 接口他天天用但底层到底发生了什么他其实不清楚。这个问题的坑在于大多数人以为流式输出就是边收边吐字但背后的机制比你想象的要复杂。今天把这个彻底说清楚。一、通用模型原理层流式处理这个东西说到底就是三层管道在运作。面试官问你怎么逐 token 处理其实问的就是你对这三层管道的理解程度。字节流 → 事件流 → 状态机 → 业务逻辑(网络层) (协议层) (语义层) (你的代码)‣ 第一层字节流 → 事件流底层收到的是原始字节嘛就是 HTTP chunked 或者 SSE 这些东西。这一层要做的事情其实就三件第一件呢是按分隔符把完整消息给切出来。通常来说就是按换行来切分的在 SSE 协议里面每条消息是以data:开头的。第二件是去处理那个不完整的尾巴。这里有个地方比较烦人就是网络分片和逻辑行边界经常对不上。举个例子一次read()可能只读到半行数据这半行你就得缓存起来等下一次读取的时候再把它拼接上然后才能去切分。说实话这地方是最容易出 bug 的——面试官追问的buffer 里半行数据怎么处理指的就是这里。第三件就是把切出来的每条完整消息 parse 成 JSON。这样你就能拿到类似{type: content_block_delta, index: 0, delta: {...}}这样的事件对象了。‣ 第二层事件流 → 状态机拿到事件之后呢你就得去维护一个状态机了。这个状态机的作用就是记录当前正在构建什么东西。为什么要这样呢因为流式协议是分块、分步骤来广播的你得跟踪每一步的状态变化。具体来说就是这么几个事件类型message_start这个事件来了你就初始化一个空的响应对象。content_block_start来了呢就开始一个新的内容块可能是文本也可能是工具调用。你要记下它的类型和 index。然后content_block_delta到了就把增量内容追加到对应 index 的块里面去。这个增量内容可能是文本片段也可能是 JSON 片段。等到content_block_stop来了当前这个块就算结束了。注意哈只有到这个时候你才能确认这块内容是完整的。最后message_delta或者message_stop来了整条消息就结束了可能会带着 usage、stop_reason 这些元信息。这里有个关键原则你一定要记住也是面试官反复追问的点只有块结束的时候才能保证内容是完整的。尤其是工具调用的参数它是逐字符流式吐出来的 JSON 片段就是那个input_json_delta。如果你中途就尝试去 parse 的话大概率会因为 JSON 不完整而失败。你必须等content_block_stop了数据攒够了再去解析。还有一点要注意的就是多个 content block 可能是并行存在的它们靠 index 来区分。你不能假设它们是严格顺序、互不重叠的你的状态机要能同时维护多个块的进度才行。‣ 第三层状态机 → 业务逻辑这一层就完全是你自己来决定怎么用这些确定下来的增量数据了。网易面试官问的怎么逐 token 处理其实问的就是这一层。有几个常见的场景实时展示的话文本类的 delta 可以在到达的瞬间就往 UI 里面追加做打字机效果。这个不需要等块结束到了就能用。工具调用检测就不一样了你必须等对应的 content block 结束也就是等到content_block_stop。然后从累积好的 buffer 里面取出完整的 JSON再去执行工具调用。这就是面试官说的工具调用的参数也是流式返回的你不能每个 token 就 parse。还有个场景是提前终止。就是说如果你检测到某个关键词或者已经拿到了你需要的信息你可以主动去断开连接不用等整个流结束。最后一个是背压处理。如果你的下游处理比如写数据库、调工具、调外部 API 这些操作比 token 到达的速度慢你就得有个缓冲队列。不然的话会阻塞底层的read()导致连接层面卡住。◆ ◆ ◆二、两种实现方式只是谁来做前两层的区别‣ 方式 A用 SDK推荐日常首选SDK 已经把第一层和第二层封装好了嘛直接给你处理干净的结果。你只管写第三层的业务逻辑就行了。面试的时候你能说清楚 SDK 帮你做了什么、你还需要自己做什么就够了。with client.messages.stream( modelclaude-sonnet-4-6, max_tokens1024, messages[{role: user, content: ...}]) as stream: for text in stream.text_stream: # 第三层你的业务逻辑 process_token(text) # SDK 已经帮你做完第一、二层 final_message stream.get_final_message() plaintext const stream client.messages.stream({...});stream.on(text, (text) { process_token(text); // 同样第一二层已经被 SDK 吃掉了});‣ 方式 B手写 SSE没有官方 SDK 时才需要这种情况下你就得自己去实现第一层和第二层了。网易面试官追问的buffer 里半行数据怎么处理“怎么判断 JSON 来完了”全在这段代码里。// 第一层字节流 → 事件流const reader response.body.getReader();const decoder new TextDecoder();let buffer ;// 第二层的状态正在构建的 content blocks用 index 做 key支持并行const blocks {};while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); // 不完整的尾巴留到下次 for (const line of lines) { if (!line.startsWith(data: )) continue; const event JSON.parse(line.slice(6)); // 第二层状态机维护块边界 switch (event.type) { case content_block_start: blocks[event.index] { type: event.content_block.type, buffer: }; break; case content_block_delta: const block blocks[event.index]; if (event.delta.type text_delta) { block.buffer event.delta.text; process_token(event.delta.text); // 第三层文本可以边到边用 } else if (event.delta.type input_json_delta) { block.buffer event.delta.partial_json; // 工具参数先攒着别急着解析 } break; case content_block_stop: const finishedBlock blocks[event.index]; if (finishedBlock.type tool_use) { const toolInput JSON.parse(finishedBlock.buffer); // 第三层块结束才解析 executeTool(toolInput); } break; } }}◆ ◆ ◆三、对照表一眼看懂谁负责什么通用模型的层方式 ASDK方式 B手写 SSE字节流→事件流SDK 内部完成你写reader.read() buffer 拼接事件流→状态机SDK 内部完成你写switch(event.type)维护 block 状态状态机→业务逻辑你写for text in stream.text_stream你写process_token/executeTool◆ ◆ ◆四、怎么选默认就用 SDK 好了。第一层和第二层的那些坑比如 JSON 截断、多块并行索引、编码问题这些SDK 都已经帮你踩过了你没必要再去重复造轮子。那什么时候手写 SSE 呢就是你接入的模型没有官方 SDK 的时候或者是你在自建代理又或者你需要对协议做一些特殊的改造这种情况下才需要自己写。不过不管你怎么选第三层永远是要你自己来写的。这才是 Agent 真正的业务逻辑所在就是怎么展示、怎么触发工具、怎么做提前终止判断这些。用一句话来总结的话通用模型讲的是必须做什么SDK 和手写 SSE 只是谁来做的选择。本质上它们是同一套三层管道SDK 帮你做了前两层手写的话你就得自己实现全部三层。。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】
网易二面追问:开发 Agent 的时候,LLM 返回的是流式响应,你应该用什么方式逐 token 处理输出?
发布时间:2026/7/15 20:20:58
前几天有个同事去面网易的 Agent 开发岗一面聊得挺顺利二面的时候面试官问了一个看起来很基础的问题“开发 Agent 的时候LLM 返回的是流式响应你应该用什么方式逐 token 处理输出”他想都没想就答了“用 stream 接口嘛for 循环遍历 text_stream每个 token 到了就处理。”面试官点点头又问了一句“那工具调用的参数也是流式返回的你也是每个 token 到了就 parse 吗”他愣了一下说“工具调用……不也是文本吗应该差不多吧”面试官没有直接说对不对只是又问了一个问题“那如果 JSON 只来了一半你 parse 会怎样”他挠了挠头说会报错。面试官接着问“那你怎么办等它来完怎么判断它来完了”他突然意识到自己从来没认真想过这个问题——stream 接口他天天用但底层到底发生了什么他其实不清楚。这个问题的坑在于大多数人以为流式输出就是边收边吐字但背后的机制比你想象的要复杂。今天把这个彻底说清楚。一、通用模型原理层流式处理这个东西说到底就是三层管道在运作。面试官问你怎么逐 token 处理其实问的就是你对这三层管道的理解程度。字节流 → 事件流 → 状态机 → 业务逻辑(网络层) (协议层) (语义层) (你的代码)‣ 第一层字节流 → 事件流底层收到的是原始字节嘛就是 HTTP chunked 或者 SSE 这些东西。这一层要做的事情其实就三件第一件呢是按分隔符把完整消息给切出来。通常来说就是按换行来切分的在 SSE 协议里面每条消息是以data:开头的。第二件是去处理那个不完整的尾巴。这里有个地方比较烦人就是网络分片和逻辑行边界经常对不上。举个例子一次read()可能只读到半行数据这半行你就得缓存起来等下一次读取的时候再把它拼接上然后才能去切分。说实话这地方是最容易出 bug 的——面试官追问的buffer 里半行数据怎么处理指的就是这里。第三件就是把切出来的每条完整消息 parse 成 JSON。这样你就能拿到类似{type: content_block_delta, index: 0, delta: {...}}这样的事件对象了。‣ 第二层事件流 → 状态机拿到事件之后呢你就得去维护一个状态机了。这个状态机的作用就是记录当前正在构建什么东西。为什么要这样呢因为流式协议是分块、分步骤来广播的你得跟踪每一步的状态变化。具体来说就是这么几个事件类型message_start这个事件来了你就初始化一个空的响应对象。content_block_start来了呢就开始一个新的内容块可能是文本也可能是工具调用。你要记下它的类型和 index。然后content_block_delta到了就把增量内容追加到对应 index 的块里面去。这个增量内容可能是文本片段也可能是 JSON 片段。等到content_block_stop来了当前这个块就算结束了。注意哈只有到这个时候你才能确认这块内容是完整的。最后message_delta或者message_stop来了整条消息就结束了可能会带着 usage、stop_reason 这些元信息。这里有个关键原则你一定要记住也是面试官反复追问的点只有块结束的时候才能保证内容是完整的。尤其是工具调用的参数它是逐字符流式吐出来的 JSON 片段就是那个input_json_delta。如果你中途就尝试去 parse 的话大概率会因为 JSON 不完整而失败。你必须等content_block_stop了数据攒够了再去解析。还有一点要注意的就是多个 content block 可能是并行存在的它们靠 index 来区分。你不能假设它们是严格顺序、互不重叠的你的状态机要能同时维护多个块的进度才行。‣ 第三层状态机 → 业务逻辑这一层就完全是你自己来决定怎么用这些确定下来的增量数据了。网易面试官问的怎么逐 token 处理其实问的就是这一层。有几个常见的场景实时展示的话文本类的 delta 可以在到达的瞬间就往 UI 里面追加做打字机效果。这个不需要等块结束到了就能用。工具调用检测就不一样了你必须等对应的 content block 结束也就是等到content_block_stop。然后从累积好的 buffer 里面取出完整的 JSON再去执行工具调用。这就是面试官说的工具调用的参数也是流式返回的你不能每个 token 就 parse。还有个场景是提前终止。就是说如果你检测到某个关键词或者已经拿到了你需要的信息你可以主动去断开连接不用等整个流结束。最后一个是背压处理。如果你的下游处理比如写数据库、调工具、调外部 API 这些操作比 token 到达的速度慢你就得有个缓冲队列。不然的话会阻塞底层的read()导致连接层面卡住。◆ ◆ ◆二、两种实现方式只是谁来做前两层的区别‣ 方式 A用 SDK推荐日常首选SDK 已经把第一层和第二层封装好了嘛直接给你处理干净的结果。你只管写第三层的业务逻辑就行了。面试的时候你能说清楚 SDK 帮你做了什么、你还需要自己做什么就够了。with client.messages.stream( modelclaude-sonnet-4-6, max_tokens1024, messages[{role: user, content: ...}]) as stream: for text in stream.text_stream: # 第三层你的业务逻辑 process_token(text) # SDK 已经帮你做完第一、二层 final_message stream.get_final_message() plaintext const stream client.messages.stream({...});stream.on(text, (text) { process_token(text); // 同样第一二层已经被 SDK 吃掉了});‣ 方式 B手写 SSE没有官方 SDK 时才需要这种情况下你就得自己去实现第一层和第二层了。网易面试官追问的buffer 里半行数据怎么处理“怎么判断 JSON 来完了”全在这段代码里。// 第一层字节流 → 事件流const reader response.body.getReader();const decoder new TextDecoder();let buffer ;// 第二层的状态正在构建的 content blocks用 index 做 key支持并行const blocks {};while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); // 不完整的尾巴留到下次 for (const line of lines) { if (!line.startsWith(data: )) continue; const event JSON.parse(line.slice(6)); // 第二层状态机维护块边界 switch (event.type) { case content_block_start: blocks[event.index] { type: event.content_block.type, buffer: }; break; case content_block_delta: const block blocks[event.index]; if (event.delta.type text_delta) { block.buffer event.delta.text; process_token(event.delta.text); // 第三层文本可以边到边用 } else if (event.delta.type input_json_delta) { block.buffer event.delta.partial_json; // 工具参数先攒着别急着解析 } break; case content_block_stop: const finishedBlock blocks[event.index]; if (finishedBlock.type tool_use) { const toolInput JSON.parse(finishedBlock.buffer); // 第三层块结束才解析 executeTool(toolInput); } break; } }}◆ ◆ ◆三、对照表一眼看懂谁负责什么通用模型的层方式 ASDK方式 B手写 SSE字节流→事件流SDK 内部完成你写reader.read() buffer 拼接事件流→状态机SDK 内部完成你写switch(event.type)维护 block 状态状态机→业务逻辑你写for text in stream.text_stream你写process_token/executeTool◆ ◆ ◆四、怎么选默认就用 SDK 好了。第一层和第二层的那些坑比如 JSON 截断、多块并行索引、编码问题这些SDK 都已经帮你踩过了你没必要再去重复造轮子。那什么时候手写 SSE 呢就是你接入的模型没有官方 SDK 的时候或者是你在自建代理又或者你需要对协议做一些特殊的改造这种情况下才需要自己写。不过不管你怎么选第三层永远是要你自己来写的。这才是 Agent 真正的业务逻辑所在就是怎么展示、怎么触发工具、怎么做提前终止判断这些。用一句话来总结的话通用模型讲的是必须做什么SDK 和手写 SSE 只是谁来做的选择。本质上它们是同一套三层管道SDK 帮你做了前两层手写的话你就得自己实现全部三层。。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】