LangChain 篇
对应课件:LangChain指南/01-12。LangChain 题通常不会问太深——面试官用它确认你写过真实的链路、踩过真实的坑。本篇 4 题集中在最容易考出区分度的点:LCEL 的设计思想、流式并发的原理、结构化输出…
对应课件:LangChain指南/01-12。LangChain 题通常不会问太深——面试官用它确认你写过真实的链路、踩过真实的坑。本篇 4 题集中在最容易考出区分度的点:LCEL 的设计思想、流式并发的原理、结构化输出的校验、对话记忆的本质。
Q01 · LCEL 是什么?为什么链要用 | 管道拼?
难度:★★★ | 频率:高 | 对应课件:LangChain指南/03
面试官:你写的 LangChain 代码全是 (prompt | model | parser) 这种管道。LCEL 解决什么问题?
第 1 层追问:Runnable 统一接口
先说 LCEL 的核心抽象。
参考回答:LCEL(LangChain Expression Language)的核心是 Runnable 统一接口:prompt、model、parser、retriever——全部实现同一套方法(invoke / stream / batch)。有了统一接口,任意组件用 | 拼接就自动组合成新 Runnable——组合后的链本身也是 Runnable,可以继续拼。RunnableParallel 让多个分支并行、RunnablePassthrough 原样传输入。本质是组合子设计模式:像 Unix 管道一样,grep | sort | head 每个环节只管自己的输入输出形状。
第 2 层追问:接口形状意识
拼管道时你实际要注意什么?
参考回答:每一环的"吃什么、吐什么"要心里有数,这是拼链不报错的关键。以 RAG 链为例:RunnableParallel(context=retriever | format_docs, question=RunnablePassthrough) 的输入是裸字符串(不是 dict!两个分支拿到的都是原始输入);retriever 吐 list[Document],所以必须站一个 format_docs 转成字符串再进 prompt;prompt 吃 dict 吐 ChatPromptValue;model 吐 AIMessage;StrOutputParser 吐 str。漏掉 format_docs 是新手最经典的报错——模板变量里出现一堆 Document 对象的字符串表示,就是它。
第 3 层追问:为什么值得引入这层抽象?
自己写函数串联不是更直接?
参考回答:三个真实收益。一,一行换执行模式:invoke 改 stream 就变流式、改 batch 就并发批处理——组件级实现一次,执行模式全链路自动获得,自己写串行函数没有这个红利。二,组合复用:检索分支、格式化函数、解析器都是 Runnable,随时重组新链。三,可观测挂钩:回调系统挂在 Runnable 层,一条链的执行事件(哪环开始、耗时、输入输出)自动可追踪。代价是调试时栈深、抽象层多——所以课程的建议:LCEL 适合"数据在组件间直线流动"的场景,逻辑复杂了上 LangGraph,两者是互补不是替代。
考察意图与踩坑点
- 考察意图:考你写的是"组件组合"还是"抄来的模板"。接口形状答得出来,说明真拼过链、真排过错。
- 踩坑点:说不出 Runnable 三件套方法;不知道 RunnableParallel 的分支拿的是原始输入;分不清 LCEL 和 LangGraph 的分工。
- 一句话总结:Runnable 统一接口 → 一切皆可
|组合 → stream/batch 全链路白拿;拼链先画接口形状表。
Q02 · stream 和 batch 底层是怎么工作的?
难度:★★★ | 频率:中 | 对应课件:LangChain指南/04
面试官:链的流式输出是怎么实现的?batch 并发又发生在哪一层?
第 1 层追问:流式的原理
stream() 拿到的逐块输出从哪来?
参考回答:流式的源头是模型的逐 token 自回归生成——模型每预测出一个 token 就可以往外吐(KV Cache 让后续计算不用重算历史,见 LLM 原理篇 Q07)。链上每一环的 stream() 逐块传递:parser 逐块处理 model 吐的增量。工程价值两块:首字延迟从"全文生成完"降到"第一个 token 出来"(长输出的体验差异是秒级到百毫秒级);用户感知——逐字打出比转圈等待的体感好一个档次。注意流式的代价:拿到的是增量块,需要自己聚合 + 处理半截结构(JSON 流到一半是不合法的)。
第 2 层追问:batch 的并发在哪?
batch 一次传 50 个问题,快在哪?
参考回答:两层。组件层:batch 对输入列表做并发调用(默认线程池/异步),50 个问题同时发出去而不是串行排队——LLM 推理是 IO 密集型(每请求几十秒的等待),并发把总时间从"50 × 单次"压到接近"单次 + 排队"。流内层:RunnableParallel 的多个分支并发执行(比如同时做检索和改写)。要区分两个正交维度:batch 是"多输入并发",stream 是"单输出增量"——可以组合(并发跑 50 个流式任务)。爬坡要点:并发不是越大越好——LLM 服务有速率限制,批量嵌入要用 asyncio.Semaphore 限流,打爆限速反而更慢。
第 3 层追问:什么场景选什么执行模式?
给个选型直觉。
参考回答:聊天界面:stream——用户等待焦虑是第一敌人。批量处理任务(评测评集、语料入库、报告批产):batch——总吞吐优先,还能配合重试和断点。混合:网页上并发展示多个 Agent 任务——batch + stream 组合。唯一不用流的场景:需要完整 JSON 做后处理的同步链(校验必须在全文之后),此时流式只是给用户看进度条,程序逻辑等 invoke 语义。
考察意图与踩坑点
- 考察意图:考"用框架但知道框架下面是什么"——流式连到自回归 token、batch 连到 IO 并发,两条线都通才算懂。
- 踩坑点:以为 stream 是"服务端做好了流"不知道逐 token 来源;把 batch 和 RunnableParallel 混为一谈;并发不限流被 429 毒打过的经历说不出来。
- 一句话总结:stream 源于逐 token 自回归、batch 是多输入 IO 并发——两个正交维度,聊天用流、批产用并发、限流用 Semaphore。
Q03 · 结构化输出怎么做?直接让模型"输出 JSON"可靠吗?
难度:★★★ | 频率:高 | 对应课件:LangChain指南/05、agent核心/06
面试官:你要从用户的话里提取 {城市, 日期} 这种结构。直接提示词里写"请输出 JSON"够吗?
第 1 层追问:裸提示词的问题
为什么不可靠?
参考回答:模型是概率系统——提示词约束是软约束。裸写"请输出 JSON"会遇到全套:输出前后带废话("好的,以下是 JSON:")、JSON 里带注释(不合法)、字段名拼写漂移、数字带单位引号、中文引号直引号混用。下游代码要 json.loads,任何一种都崩。这不是模型"不听话",是概率系统对"格式完美"没有天然保证——所以结构化输出必须是约束 + 校验 + 兜底的三层工程,不能只靠一句提示。
第 2 层追问:约束层怎么做?
有哪些硬一点的手段?
参考回答:按硬度排序。一,输出解析器:Pydantic 定义 schema,解析器做提取 + 校验 + 失败重试(把解析错误回传给模型重试一次)。二,函数调用式提取:用 Tool Calling 协议——强制 tool_choice 指定一个"提取器函数",模型被后训练过输出调用格式,可靠性显著高于裸提示(这是 agent核心/07 的实用技巧:自然语言 → 结构化数据的转换器)。三,服务端约束解码:支持 structured output / JSON mode 的推理服务在解码层禁止非法 token——语法上不可能生成不合法 JSON,硬度最高,但要确认服务支持且不牺牲质量。LangChain 里对应 PydanticOutputParser / with_structured_output。
第 3 层追问:校验与兜底?
就算有了约束,应用层还要做什么?
参考回答:三层防线收尾。一,Pydantic 做语义校验:字段类型只是底线,还要加业务约束(日期不能是过去、城市要在枚举里)——field_validator 把"格式对但值荒谬"挡下来。二,失败重试:校验不过,把具体错误信息回传给模型修一轮("日期格式应为 YYYY-MM-DD"比"格式错误"有效得多);重试 2 次仍失败走兜底。三,兜底路径:降级为追问用户("抱歉没听清日期,请问是哪天?")——宁可多一轮对话,不要带着脏数据入库。这套"校验 + 反馈式重试 + 降级"思想在 Tool Calling 的参数校验里同样适用,是应用层的通用模式。
考察意图与踩坑点
- 考察意图:结构化输出是 AI 应用最高频的工程需求(每个表单填充、数据抽取场景都要),考"软约束意识"的成熟度。
- 踩坑点:说"我在提示词里强调了很多遍 JSON"(软约束当硬约束用);不知道 tool_choice 强制调用这个技巧;校验失败没有重试/兜底设计。
- 一句话总结:约束(解析器/tool calling/解码层)+ Pydantic 语义校验 + 反馈式重试 + 追问兜底——结构化输出是三层工程不是一句提示词。
Q04 · 对话记忆是怎么实现的?"模型记得我"到底记在哪?
难度:★★★ | 频率:高 | 对应课件:LangChain指南/09
面试官:用户说"它记得上次聊的"。记忆到底存在哪?多轮对话的机制说清楚。
第 1 层追问:无状态本质
先说模型记不记得。
参考回答:模型本身零记忆。每次 API 调用是独立的——服务端不保存你的对话,所谓"记得",是你的应用把历史 messages 列表每轮重发。消息按 role 组织成序列(system / user / assistant / tool),完整历史作为上下文传入,模型"读到"了之前的内容才表现出记忆。验证方式很直接:新开一个会话不传历史,模型立刻失忆——记忆是应用的工程产物,不是模型的能力。这个认知决定后面所有记忆方案的设计。
第 2 层追问:维护历史的具体规矩
那维护这个 messages 列表有什么规矩?
参考回答:两条铁律。一,每轮都要追加 + 回账:收到用户消息 append 进列表,连同历史一起发送,漏了任何一边模型就"失忆"或"复读"——比如忘了 append 用户消息,下一轮模型完全不知道刚才说了什么。二,工具调用的消息配对完整性:assistant 发起 tool_call 后必须有对应 tool 消息(带 tool_call_id),缺一条直接协议报错。课程里最经典的坑就在这:保存历史时要把 tool_call 的完整配对一起存,只存文本丢掉 tool 环节,恢复会话时第一轮就崩。
第 3 层追问:窗口满了怎么办?
历史无限增长,怎么管理?
参考回答:分三档按需升级。一,滑动窗口:trim_messages 按 token 数裁剪,strategy="last" 保最近 N 轮,token_counter 决定计数的精确度——实现最简,丢早期信息的代价要认。二,摘要压缩:更早的历史定期总结替换原文。三,外置记忆:事实写入存储、按需检索注入(配 RAG)。裁剪时的保底纪律:system prompt 和关键约束单独保留,永远不进裁剪范围——否则"你是客服、只依据知识库回答"这类身份约束跟着旧消息一起被剪掉,模型行为当场漂移。更深一层见 LLM 原理篇 Q06——两层互为印证,面试里连着答效果翻倍。
考察意图与踩坑点
- 考察意图:这是考察"对无状态机制是否有切肤认知"的送分/送命题——一句话答错("模型记得")直接暴露没做过多轮应用。
- 踩坑点:说"模型有上下文记忆所以记得";不知道 tool_call 配对完整性这个坑;裁剪把 system prompt 一起裁了。
- 一句话总结:模型零记忆,记忆 = 应用每轮重发 messages;append 别漏、tool 配对别缺、system prompt 永不裁。
本篇速记卡
| 题 | 一句话核心 |
|---|---|
| Q01 LCEL | Runnable 统一接口,| 组合,stream/batch 白拿;拼链先画接口形状 |
| Q02 流式/并发 | stream 源自逐 token 自回归,batch 是 IO 并发;限流用 Semaphore |
| Q03 结构化输出 | 约束 + Pydantic 语义校验 + 反馈式重试 + 追问兜底,三层工程 |
| Q04 对话记忆 | 模型零记忆,应用重发 messages;tool 配对完整、system 永不裁 |