怎么用这份练习
原则:先看题,合上答案,动手写 15 分钟。实在不会再翻提示;还不会再看答案,然后合上答案自己复述一遍。
- 🟢 入门(10 题):走完手把手教程 Step 1-6 就能做。用来确认基础没漏。
- 🟡 进阶(6 题):走完 Step 7-11 能做。考察 LangGraph 的结构化思维。
- 🔴 深度(4 题):走完 Step 12 + 面试前夕做。对标 50K+ 岗位白板追问。
每题结构:题目 → 💡 提示 → ✅ 参考答案 → 📊 评分标准。
如果还没做过手把手教程,请先去 📘 零基础手把手版。
🟢 入门题(10 题)
E1. invoke 和 stream 区别是什么?什么时候用哪个?
💡 提示
想想"用户体验"维度:一个是等全部、一个是一个字一个字出。
✅ 参考答案
invoke:同步等完整结果返回。适合批量任务、结构化输出、后台脚本。stream:流式返回 token。适合面向用户的聊天 UI,减少首字等待时间(TTFT)。
真实场景选择:
- 后端 API 批处理 →
invoke - 聊天界面 →
stream(配合 SSE/WebSocket) - 结构化输出(JSON / Pydantic)→
invoke(流式意义不大)
📊 评分:说清场景差异 = 合格;能说到 TTFT 或后端适配 = 优秀。
E2. 下面代码哪里会报错?怎么修?
prompt = ChatPromptTemplate.from_template("返回 JSON:{{\"q\":\"{q}\"}}")
chain = prompt | llm
result = chain.invoke({"q": "你好"})
print(result["q"])💡 提示
chain 的输出是什么类型?
✅ 参考答案
chain 的输出是 AIMessage,不是 dict,result["q"] 会报 TypeError。
修法 1:加 parser:
from langchain_core.output_parsers import JsonOutputParser
chain = prompt | llm | JsonOutputParser()修法 2:直接访问:
print(result.content)📊 评分:找到类型错误 = 合格;给出两种修法 = 优秀。
E3. 什么是 LCEL?请用 Linux 管道类比解释。
✅ 参考答案
LCEL = LangChain Expression Language,用 | 把 Runnable 串成 chain,左边的输出作为右边的输入。
类比 Linux 管道:单向管道,每一段只关心自己的输入输出。LangChain 中 prompt | llm | parser 就是 dict → Prompt → AIMessage → str 的类型流转。
📊 评分:说清类比 + 输入输出对接 = 合格。
E4. 为什么要用 with_structured_output 而不是让 LLM 自由返回 JSON?
✅ 参考答案
- 自由 JSON:靠 prompt "求"模型返回结构,偶尔多字段、少引号、Markdown 包裹,要自己容错。
with_structured_output:走模型原生 tool calling 协议,模型端保证结构合法。Pydantic 校验一次即通过。
生产环境几乎必用结构化输出。唯一不用的场景:需要长文本自由创作。
📊 评分:说到 tool calling 协议 = 优秀。
E5. RunnableWithMessageHistory 怎么区分不同用户的对话?
✅ 参考答案
通过 config.configurable.session_id 区分。每个 session_id 对应一份独立的 ChatMessageHistory。
cfg = {"configurable": {"session_id": "u-001"}}
chain.invoke({"input": "..."}, cfg)相同 session 共享历史;换 session 从零开始。
📊 评分:答到 session_id = 合格;能指出 config 结构 = 优秀。
E6. RAG 的 chunk_size 设多大合适?chunk_overlap 为什么需要?
💡 提示
想想如果一句话刚好被切成两半会发生什么。
✅ 参考答案
- chunk_size:常用 200-800 字符(中文)或 256-1024 token。取决于文档结构:
- 散文多 / 概念密集 → 偏小(200-400),提高召回精度。
- 长代码 / 长流程 → 偏大(800-1200),保证完整上下文。
- chunk_overlap:通常 10-20% 的 chunk_size。作用是防止一句完整的话被切到两个 chunk,导致检索时上下文残缺。
经验值:中文知识库起步用 chunk_size=500, overlap=80。
📊 评分:答出范围 + overlap 作用 = 合格;能按文档类型分情况 = 优秀。
E7. 模型能"自己"执行你的 Python 函数吗?
✅ 参考答案
不能。模型只是返回"我想调用这个函数,参数是 X"的意图(tool_call),真正执行是你的代码干的。
完整流程:
- 你给模型工具清单 → 模型返回
tool_calls - 你的代码解析 tool_calls,调真实函数
- 把函数结果塞回 messages
- 再调一次模型,让它基于结果回答
把模型当成"开处方的医生",你是"抓药的药剂师"。
📊 评分:说清"模型只开处方 / 执行在外部" = 合格。
E8. 下面 chain 里,RunnablePassthrough() 起什么作用?
rag = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | parser
)
rag.invoke("什么是 RAG?")✅ 参考答案
RunnablePassthrough() 把输入原样传递给下游。
这里 invoke 传进来的字符串 "什么是 RAG?" 会:
- 通过
retriever | format_docs被检索并格式化,填进context。 - 同时通过
RunnablePassthrough()原样塞进question。
等价于手动写:
inputs = {"context": format_docs(retriever.invoke(q)), "question": q}Passthrough 让 "同一个输入同时分发到多个下游"变得声明式。
📊 评分:能说清"同一个输入两路分发" = 合格。
E9. 如果 LLM 返回的 JSON 不符合 Pydantic schema,with_structured_output 会怎样?
✅ 参考答案
通常有两种实现:
- function_calling 模式(OpenAI / 多数现代模型):schema 在模型端就约束了,基本不会返回非法结构。
- json_mode / json_schema 模式:偶尔会偏差;LangChain 会尝试反序列化,失败则抛
OutputParserException。
实战应对:
- 加
method="function_calling"显式走工具调用(如果模型支持)。 - 业务层包一层重试:
try/except OutputParserException后 retry 一次,仍失败才降级。 - 打 metric:结构化失败率超过 1% 就告警。
📊 评分:能说出两种模式差异 + 重试策略 = 优秀。
E10. 为什么 LangChain 的 ChatHistory 不适合做长期记忆?
💡 提示
想想 100 轮对话之后 prompt 长什么样。
✅ 参考答案
- Token 爆炸:每轮都要把全部历史塞进 prompt,100 轮就上万 token,费用和延迟双爆。
- Lost-in-middle:模型对长上下文中段的注意力会显著下降,早期信息被稀释。
- 没有语义检索:历史里有一条关键信息,第 50 轮问到时如果没被截到就完全丢了。
长期记忆正确做法:
- 用
trim_messages滚动截断(保留最近 N 条 + system)。 - 把关键事实抽成结构化记忆,存进 vector store / kv store,需要时再检索回来。
- LangGraph 用
Store做跨 thread 的长期记忆。
📊 评分:答到 token 爆炸 + lost-in-middle = 优秀。
🟡 进阶题(6 题)
A1. 下面 LangGraph 代码有 bug,找出来并修。
class State(TypedDict):
messages: list[BaseMessage]
def node_a(state):
return {"messages": [AIMessage("hi")]}
def node_b(state):
return {"messages": [AIMessage("bye")]}
g = StateGraph(State)
g.add_node("a", node_a)
g.add_node("b", node_b)
g.add_edge(START, "a")
g.add_edge("a", "b")
g.add_edge("b", END)调用后 state["messages"] 里只有 "bye","hi" 不见了。
✅ 参考答案
Bug:messages 字段没有 reducer,LangGraph 的默认合并是覆盖,所以后一个节点把前一个的值覆盖了。
修法:用 add_messages reducer(来自 langgraph.graph.message):
from langgraph.graph.message import add_messages
from typing import Annotated
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]这样两个节点的返回会被追加合并,最终 ["hi", "bye"]。
📊 评分:找到 reducer 问题 + 正确修复 = 合格;能解释"默认是覆盖" = 优秀。
A2. 有个图:classify → answer → END。需求改成:answer 返回的结果如果被 classify 判断"质量差",就跳回 answer 重新生成,最多 3 次。怎么改?
💡 提示
想想条件边 + 计数器字段。
✅ 参考答案
class State(TypedDict):
answer: str
quality: str
retry_count: int
def quality_check(state) -> str:
if state["quality"] == "good" or state["retry_count"] >= 3:
return "end"
return "retry"
def answer_node(state):
ans = llm.invoke(...)
return {"answer": ans, "retry_count": state["retry_count"] + 1}
def check_node(state):
q = judge_llm.invoke(state["answer"]) # 返回 good/bad
return {"quality": q}
g.add_node("answer", answer_node)
g.add_node("check", check_node)
g.add_edge(START, "answer")
g.add_edge("answer", "check")
g.add_conditional_edges("check", quality_check, {
"retry": "answer", # ← 回跳
"end": END,
})关键点:
- 条件边的目标字典里可以指回之前的节点(形成环)。
- 必须有终止条件(retry_count 上限),不然死循环。
📊 评分:写出环结构 + 终止条件 = 合格;用 reducer 正确累加 retry_count = 优秀。
A3. InMemorySaver / SqliteSaver / PostgresSaver 应该怎么选?
✅ 参考答案
| Checkpointer | 场景 | 优点 | 缺点 |
|---|---|---|---|
InMemorySaver | 单元测试、交互式 notebook、极简 demo | 零配置 | 进程退出全丢;不能多进程共享 |
SqliteSaver | 本地开发、单机小工具、离线 Agent | 单文件持久化,迁移方便 | 并发写有限;不适合多节点部署 |
PostgresSaver | 生产环境 | 高并发、可多节点共享、支持事务、易备份 | 需要 Postgres 基础设施 |
选型决策:
- 上线前 dev → SqliteSaver
- 正式生产 → PostgresSaver + 连接池
- 绝不把 InMemorySaver 部到生产
📊 评分:能说出生产必须 Postgres = 合格;能讨论并发/多节点 = 优秀。
A4. interrupt 是怎么实现"暂停等人"的?为什么必须配 checkpointer?
✅ 参考答案
机制:
- 节点调用
interrupt(payload)时,LangGraph 把当前 State 写进 checkpointer,把payload存进PendingInterrupt,然后抛出 GraphInterrupt。 - 外层
app.invoke()捕获到,不继续执行后续节点,返回当前 State。 - 外部系统拿到 payload,展示审批 UI。
- 审批完成后,用
Command(resume=decision)再次invoke,同 thread_id。 - LangGraph 从 checkpointer 读出 State,重新执行那个节点(这次
interrupt()直接返回decision)。
为什么必须 checkpointer:State 不持久化就没法"等"——一旦函数返回,State 就丢了。interrupt 本质是"State 持久化 + 函数暂停恢复"。
生产注意点:
- resume 可能在分钟/小时/天级延迟后发生,所以用
PostgresSaver。 - 节点要幂等:因为 resume 后该节点会重跑一遍。副作用代码(比如发邮件)要放在 interrupt 之后。
📊 评分:答到 checkpoint + 节点重跑 = 合格;提到幂等 = 优秀。
A5. 多个节点同时往 state["log"] 追加字符串,最终顺序是怎样的?
✅ 参考答案
LangGraph 支持并行节点(同一 superstep 内)。并行节点的写入合并由 reducer 决定:
log: Annotated[list[str], operator.add]- 并行 A 和 B 同时跑完,框架把它们返回的两个 list 按 superstep 顺序合并(非"严格执行时间"顺序)。
- 同一 superstep 内节点间不保证顺序——所以不能用 log 顺序表达因果。
- 跨 superstep 的顺序是严格的:superstep N 的所有节点都合并完,再进入 superstep N+1。
实战:
- 需要严格顺序 → 串行连边,不要并行。
- 日志只作观察用 → 并行可以。
- 关键因果信息要带显式字段(如 timestamp)。
📊 评分:说清 superstep 概念 = 合格;提出不能用 log 表达因果 = 优秀。
A6. 你想给 Agent 加"用户可以随时打断"的能力(类似 ChatGPT 的 Stop 按钮),用 LangGraph 怎么实现?
✅ 参考答案
三层实现:
- 流式层:用
app.astream(...)或app.stream(...)逐 step 产出,前端能收到 token/step 级事件。 - 取消信号:前端按 Stop → 后端关闭 SSE/WebSocket → 后端捕获到 client disconnect → 取消当前任务(Python 的
asyncio.CancelledError)。 - 状态清理:
- 正在运行的 tool_call 要能响应取消(工具内部要用
asyncio.sleep而非 blocking)。 - 已经持久化的 checkpoint 保留,用户再点"继续"时能从断点恢复。
- 或者标记为
cancelled状态,下次对话从新开始。
- 正在运行的 tool_call 要能响应取消(工具内部要用
LangGraph 0.2+ 提供 app.astream_events() 可以 cancel task。配合 thread_id 和 checkpointer,甚至可以"时间旅行"回到 cancel 之前的某个 state 重跑。
📊 评分:答到流式 + cancel + 恢复 三层 = 优秀。
🔴 深度题(4 题,白板级)
D1. 设计题:把一个"跑批脚本"改造成 LangGraph Agent,要点是什么?
场景:你有个 Python 脚本,每天凌晨跑:抓 10 个网页 → 清洗 → 调 LLM 打标 → 写库。现在想把它改造成 Agent。请在白板上画出:
- State 设计(字段)
- 节点划分(名字 + 职责)
- Checkpointer 选型
- 失败恢复方案
- 观测与报警
✅ 参考答案(白板要点)
State:
class State(TypedDict):
urls: list[str]
fetched: Annotated[list[Document], operator.add]
labeled: Annotated[list[dict], operator.add]
failed_urls: Annotated[list[str], operator.add]
run_id: str
stats: dict节点:
START → plan (生成 url 列表)
→ fetch (并行抓,失败写 failed_urls)
→ clean (去重/去噪)
→ label_llm (调 LLM 打标,结构化输出)
→ validate (校验字段完整)
→ write_db (落库)
→ notify (钉钉/Slack 通报)
→ ENDCheckpointer:PostgresSaver,按 run_id 作 thread_id。
失败恢复:
- 每个节点幂等(用 url hash 做 dedup key,
INSERT ... ON CONFLICT)。 - 节点失败不吞异常,写进
failed_urls或抛出到全局捕获。 - 重跑:从 checkpoint 恢复同 run_id,跳过已完成的 url(节点内做幂等判断)。
- 超时:给每个 LLM 调用加
timeout=30,fetch 加timeout=10。
观测:
- 每个节点打 trace(OpenTelemetry):开始时间、耗时、输入 hash、输出大小、status。
- 关键 metric:抓取成功率、打标耗时 p95、每日 token 费用、失败 url 数。
- 报警:失败 url > 10% 或总耗时超基线 2 倍。
把现有脚本改成 Agent 的真正好处:
- 任意节点失败可断点续跑(比每次全量重跑省 10x 成本)。
- 人审节点可加(如关键 url 打标 → interrupt → 值班同学确认)。
- State trace 在 LangSmith 可视化,排障从小时级降到分钟级。
📊 评分:
- 能画出 state + 节点 + 边 = 60 分
- 说到幂等 + 断点续跑 = 80 分
- 加观测 + 报警 + 灰度 = 100 分
D2. 你的 Agent 在 5% 的 case 上会陷入"工具调用死循环"(LLM 一直调同一个工具)。怎么排查和修?
✅ 参考答案
排查(必须带 trace):
- LangSmith 或自建 trace 里过滤
tool_call_count > 5的 thread。 - 看循环调用的工具是哪个、参数长什么样、LLM 的 reasoning 文本是什么。
- 统计哪些用户问法最容易触发。
常见根因:
- 工具描述不清:LLM 不知道"为什么上一次调用的结果已经够了"。
- 工具返回值不够明确:返回 "未找到" 太含糊,LLM 不死心再换参数试。
- Schema 字段冗余:LLM 在不同字段间反复换,以为"这次会不一样"。
- Prompt 没停止信号:没告诉 LLM "如果工具返回 X 就停止"。
修复五步:
- 给图加硬上限:全局计数器字段
tool_call_count,超过 5 次强制路由到end_with_apology节点。 - 工具返回值格式标准化:
{"status": "ok|not_found|error", "data": ..., "hint": "停止查询"}。 - 优化工具 description,写清"什么情况不要再调"。
- 加 LLM-as-Judge 检测循环:"看看这个 thread 是不是在循环?",如是则中断。
- 在 checkpoint 里保留失败案例,周度对抗集评测确保回归不变坏。
📊 评分:有 trace + 根因分析 + 多层防御 = 优秀。
D3. 解释 add_messages reducer 到底做了什么?为什么它比 operator.add 更适合消息场景?
✅ 参考答案
add_messages 不只是简单的 list 追加,它做了三件事:
- 类型容忍:接受
BaseMessage子类、dict 形式、甚至字符串,自动规整为BaseMessage。 - 按 ID 去重/替换:如果新消息和历史中某条消息
id相同,则替换而非追加。这是实现"时间旅行"和"消息编辑"的关键。 - 顺序保持:追加到末尾,保持时间顺序。
对比 operator.add:
- 单纯追加。
- 不去重,同一条消息被多次返回会出现多份。
- 不规整类型(如果节点返回 dict 而不是 BaseMessage 会后续崩)。
进阶用法:通过让节点返回带 id 的 AIMessage,可以"修改历史"——这是 LangGraph Studio 编辑功能的底层机制。
📊 评分:答到三点 + 提到编辑历史 = 优秀。
D4. 把你做的客服 Agent 投入生产,列出上线前必须搞定的 10 件事。
✅ 参考答案
基础设施(3 件):
- PostgresSaver + 连接池:不能用 Sqlite/InMemory;connection 至少 20 个起。
- 模型密钥托管:用 KMS 或 Vault,不硬编码;启动时拉取。
- 向量库集群:Chroma 换成 Qdrant/Weaviate/Milvus,带备份。
可靠性(3 件):
4. 超时与重试:每个 LLM 调用 timeout + exponential backoff;工具调用单独限额。
5. 熔断降级:连续失败 N 次自动切备用模型或返回"系统繁忙"模板。
6. 幂等与去重:外部工具(下单、退款)必须用 idempotency_key;已执行的 action 重放不重复。
安全合规(2 件): 7. Guard 输入输出:Prompt injection 过滤;PII 脱敏后再进向量库(参考 ai-application-security topic)。 8. 审计日志 WORM:谁在 thread X 对话了什么、调了什么工具、审批决策,保留 ≥ 1 年。
可观测(2 件): 9. Trace + Metrics:每 invoke 打 trace(模型/latency/tokens/cost);dashboard 追 p50/p95、token 花销、工具成功率。 10. 评测与回归:黄金集 50-100 条,每次 prompt/model 变更自动跑评测,分数低于阈值拦发布(参考 llmops-observability-evaluation topic)。
加分项:
- interrupt 的审批通知链(钉钉/邮件)+ 超时未审核兜底策略。
- 流式 SSE 接口 + 前端 cancel 路径。
- 灰度 1% → 10% → 100% 的放量计划,配回滚开关。
📊 评分:
- 列出 7 项以上 = 合格
- 涵盖基础设施+可靠性+安全+观测四类 = 优秀
- 有灰度和评测回归 = 顶级
自测分数对照
| 答对题数 | 水平 | 建议 |
|---|---|---|
| 入门 < 7 | 基础不牢 | 回头重做手把手教程 Step 1-6,别急着继续 |
| 入门 ≥ 8 且进阶 < 3 | LangChain 熟了但 LangGraph 没打通 | Step 7-11 再做一遍,重点做 A1-A3 |
| 进阶 ≥ 4 且深度 ≥ 1 | 可以投面了 | 准备 3 个带 trace 的项目故事 |
| 深度 4 题都能讲 | 接近资深水平 | 直接挑战 60K+ Agent 架构岗 |