LangGraph.js 前端 Agent 面试题锦集
精选 LangGraph.js、前端 Agent、RAG 与 LangChain 面试题,按主题分类并附工程化参考答案
一、LangGraph 基础概念
1. LangGraph.js 是什么?它解决了什么问题?
参考答案:
LangGraph.js 是面向 LLM/Agent 的有状态图工作流框架。它把“多步推理、工具调用、分支循环、人工中断、恢复执行”显式建模成图。
在工程上,它主要解决三类问题:
- 复杂流程可编排:不再局限于单次请求-响应或线性链路。
- 运行过程可恢复:借助 checkpoint 支持断点续跑。
- 行为可观测:能追踪每个节点的输入、输出和状态演进。
2. StateGraph 和 State 的区别是什么?
参考答案:
StateGraph 是“流程定义层”,描述节点、边和路由规则;State 是“运行时数据层”,承载上下文和中间结果。
可以把它理解为:
- StateGraph = 结构(执行蓝图)
- State = 数据(执行现场)
TypeScript 示例:
// StateGraph = 流程定义
import { StateGraph, Annotation, END } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
messages: Annotation<string[]>({ default: () => [] })
});
const workflow = new StateGraph(StateAnnotation)
.addNode("nodeA", (state) => ({ messages: [...state.messages, "A"] }))
.addEdge("nodeA", END);
// State = 运行时数据
const result = await workflow.compile().invoke({ messages: [] });
console.log(result.messages); // ["A"]3. 什么是 Node 和 Edge?
参考答案:
Node 是执行单元,例如:LLM 推理、工具调用、校验、数据转换;Edge 是流转规则,决定下一步去哪。
实战里重点是:
- 节点尽量单一职责,方便复用和测试。
- 边的条件要可解释,避免“隐式跳转”难排障。
4. LangGraph.js 的核心组件有哪些?
参考答案:
常见核心组件包括:
StateGraph(图定义)Annotation.Root/MessagesAnnotation(状态定义与类型推导)START/END(特殊节点标识)- Node / Edge(执行单元与流转)
- Reducer(并发状态合并)
- Checkpointer(检查点持久化,如
MemorySaver、SqliteSaver、PostgresSaver) interrupt()/Command(人机中断与恢复).compile()(图编译与配置)
5. 在 LangGraph.js 中,messages 字段为什么要配置 reducer(如 messagesStateReducer)?它如何处理“追加”和“更新”?
参考答案:
messagesStateReducer 是 messages 字段的状态合并规则,核心目标是“安全地维护消息历史”。
它的行为可以概括为两点:
- 追加:新消息默认追加到已有消息列表,而不是覆盖整个历史。
- 更新:若新旧消息
id相同,会按id做替换/更新,避免重复脏数据。
它的工程价值在于:多轮对话历史可持续累积,并发分支回写更可控,且能降低消息状态被误覆盖或重复写入的风险。
代码示例:
import { MessagesAnnotation } from "@langchain/langgraph";
// MessagesAnnotation 内置 messagesStateReducer
const workflow = new StateGraph(MessagesAnnotation);
// 追加行为
const state1 = { messages: [{ role: "user", content: "Hi" }] };
const update1 = { messages: [{ role: "assistant", content: "Hello" }] };
// 结果: [user msg, assistant msg] ✅ 追加
// 更新行为(相同 ID)
const state2 = { messages: [{ id: "msg1", role: "user", content: "Hi" }] };
const update2 = { messages: [{ id: "msg1", role: "user", content: "Hello" }] };
// 结果: [{ id: "msg1", content: "Hello" }] ✅ 替换6. 什么是 thread_id?为什么它很关键?
参考答案:
thread_id 是线程级会话标识,用于把同一条执行链路关联起来。
没有它会直接影响:
- 中断后的恢复(resume 找不到上下文)
- 多轮记忆关联
- 审计与问题回放
代码示例:
// 标准 thread_id 传递格式
await app.invoke(
{ messages: [{ role: "user", content: "Hello" }] },
{
configurable: {
thread_id: "user-123-session-456"
}
}
);
// 流式调用同样需要 thread_id
for await (const event of app.stream(input, {
configurable: { thread_id: "user-123-session-456" }
})) {
console.log(event);
}7. 什么是 checkpoint?
参考答案:
checkpoint 是某一步执行后的状态快照(如 values、next、metadata 等)。
典型用途:
- 故障恢复与断点续跑
- 时间旅行调试(回放历史状态)
- 人工中断后的安全恢复
8. 短期记忆和长期记忆有什么区别?
参考答案:
短期记忆通常是线程级(thread-scoped),围绕当前会话;长期记忆是跨线程共享的稳定信息(如用户偏好、账号画像)。
常见实现:
- 短期记忆:依赖 checkpointer。
- 长期记忆:放在独立 store,并做 namespace 隔离。
9. LangGraph.js 与 LCEL 相比有什么优势?
参考答案:
LCEL 更适合线性或轻量编排;LangGraph 更适合包含循环、条件路由、持久化和 HITL 的复杂 Agent。
一句话概括:LCEL 偏“链式表达”,LangGraph 偏“状态化流程控制”。
10. LangGraph.js 相比通用工作流框架(如 Temporal)有什么特点?
参考答案:
LangGraph 是围绕 Agent 场景原生设计的,天然支持工具调用语义、对话状态、流式交互和中断恢复。
Temporal 等框架通用性强,但需要额外建模才能贴近 LLM/Agent 语义。
二、控制流与运行时
11. 如何实现条件分支(Conditional Edges)?
参考答案:
在条件边里读取当前 state,根据条件返回目标节点标识即可动态路由。
常见分支依据:
- 是否需要调用工具
- 输出置信度是否达标
- 是否触发人工审批
API 示例:
import { StateGraph, Annotation, END } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
confidence: Annotation<number>(),
needsReview: Annotation<boolean>()
});
// 路由函数
function routeByConfidence(state: typeof StateAnnotation.State) {
if (state.confidence > 0.8) return "publish";
if (state.confidence > 0.5) return "review";
return "reject";
}
const workflow = new StateGraph(StateAnnotation)
.addNode("analyze", (state) => ({ confidence: 0.6, needsReview: true }))
.addNode("review", (state) => ({ confidence: 0.9 }))
.addNode("publish", (state) => state)
.addNode("reject", (state) => state)
.addConditionalEdges("analyze", routeByConfidence, {
publish: "publish",
review: "review",
reject: "reject"
})
.addEdge("review", "publish")
.addEdge("publish", END)
.addEdge("reject", END);常见分支依据:
- 是否需要调用工具(
state.messages.at(-1)?.tool_calls?.length) - 输出置信度是否达标
- 是否触发人工审批
12. 如何实现循环(Loop)?
参考答案:
让条件边在满足条件时回到前序节点形成闭环。
工程上必须加“停止条件”:
- 最大迭代次数
- 质量阈值达标
- 超时/预算上限
LangGraph.js 原生控制:
// 设置最大迭代次数(防止无限循环)
const app = workflow.compile({
recursionLimit: 10 // 默认 25
});
// 或在调用时覆盖
await app.invoke(input, {
recursionLimit: 5
});
// 超过限制会抛出 GraphRecursionError停止条件建议:
- 最大迭代次数(
recursionLimit) - 质量阈值达标(如置信度 > 0.9)
- 超时/预算上限(如 token 消耗 > 10000)
13. 如何实现并行执行(Parallel Execution)?
参考答案:
从同一节点扇出多个分支并发执行,在汇聚节点做结果合并。
关键点在于:
- 为共享状态键定义 reducer
- 处理慢分支超时
- 明确失败分支对全局结果的影响策略
14. 什么是 Super-step?
参考答案:
Super-step 可以理解为“一轮并发执行批次”。同批可运行节点执行完,再进入下一批。
它有助于你定位并行同步点,理解“为什么某些节点要等一轮后才执行”。
15. Orchestrator-Worker(扇出/扇入)模式如何建模?
参考答案:
Orchestrator 负责拆分任务、分发 Worker、汇总结果。Worker 负责执行具体子任务。
该模式适用于:
- 批量检索
- 多文档并行分析
- 多工具并行处理后统一裁决
16. 如何限制并行分支的并发数?
参考答案:
常见手段:
- 运行时并发配置
- 分批(batch)执行
- 队列/信号量限流
这样可避免上游 API QPS 限制、连接池耗尽和突发成本飙升。
17. LangGraph 节点是如何工作的?
参考答案:
节点读取当前 state,执行本节点逻辑后返回“状态增量(delta)”,再由运行时按 reducer 合并进全局 state。
这套模型的优势是:节点更易测试,状态变化路径更清晰。
18. Pregel Runtime 在 LangGraph 中负责什么?
参考答案:
Pregel Runtime 是 LangGraph 的图执行引擎,负责节点调度、状态合并、执行推进和检查点记录。
它采用类似 Pregel 的 super-step 执行模型:每一轮先运行当前可执行节点,再汇总状态增量并按 reducer 合并,最后推进到下一轮可执行节点。
可把它看作图工作流的执行内核:
- 决定当前批次可执行节点
- 管理状态写入顺序与合并
- 推动流程持续前进
- 与 checkpoint / interrupt 协作,实现中断后恢复和长流程容错
19. LangGraph 的 Time Travel(时间旅行)能力是什么?
参考答案:
它允许基于已持久化的历史状态回到某个时间点继续执行,并走出新的分支。
典型场景:
- 线上问题复盘
- 策略 A/B 回放对比
- 人工修正后重跑后续流程
三、前端 Agent 架构与流式交互
20. 前端 Agent 为什么推荐“前端 UI + 后端 Runtime”分层?
参考答案:
因为安全和稳定边界不同:密钥、数据库写操作、私有 API、权限控制都应在后端;前端负责交互与可视化。
这会带来三个收益:
- 减少敏感信息暴露
- 更易做统一审计与鉴权
- 前端渲染可独立迭代
21. 为什么说“Agent 不是前端组件,而是状态机/工作流系统”?
参考答案:
前端组件解决的是视图问题;Agent 解决的是决策和执行问题。
把 Agent 建模成状态机后,才有能力系统化处理:重试、回滚、中断恢复、并行分支和观测。
22. 前端只做聊天窗口会有哪些短板?
参考答案:
纯消息 UI 往往看不到“过程”,用户只能看到结果。
缺失项通常包括:
- 工具调用状态
- 步骤级进度
- 错误定位与人工接管入口
- 历史回放与审计视图
23. 如何设计“可中断审批”的前端体验?
参考答案:
重要:interrupt() 必须在服务端节点中调用,浏览器环境不支持。
后端在关键节点触发 interrupt() 并持久化状态;前端通过流式事件监听到中断信号后渲染审批面板;用户提交后向服务端发送恢复请求,服务端使用同一 thread_id 调用 Command({ resume: userInput })。
代码示例:
// ❌ 错误:浏览器中无法直接使用 interrupt
// const result = await interrupt("审批");
// ✅ 正确:服务端节点
const approvalNode = async (state) => {
const userDecision = await interrupt({
type: "approval",
data: state.draftOrder
});
return { approved: userDecision };
};
// 前端恢复(发送到 API 路由)
await fetch("/api/agent/resume", {
method: "POST",
body: JSON.stringify({
thread_id: "user-123",
resume_value: "approved"
})
});要点是"线程不变、状态延续、审批有审计、服务端执行"。
24. 为什么要区分“服务端工具”和“客户端工具”?
参考答案:
服务端工具处理高风险与高权限操作,客户端工具处理本地能力与轻交互(如浏览器 API、位置、确认弹窗)。
这是一种安全边界和体验边界的折中设计。
25. LangGraph.js 常见 streamMode 有哪些?
参考答案:
常用模式包括:
updates:状态增量values:状态全量messages:token 级输出与元数据debug/custom:调试或自定义事件
26. 什么时候用 updates,什么时候用 messages?
参考答案:
updates 用于展示流程推进(节点完成、状态变化);messages 用于实时文本流。
实战里通常“双流并行”:
- 内容区消费
messages - 步骤区消费
updates
27. 前端如何避免流式渲染性能抖动?
参考答案:
典型做法:
- token 更新节流(throttle/debounce)
- 批量刷新而非逐 token 重绘
- 虚拟列表 + 分区渲染(消息区/步骤区分离)
目标是避免频繁重排(reflow)和滚动抖动。
28. 前端如何同时展示“内容流”和“步骤流”?
参考答案:
建议把展示层拆成两条通道:
- 主聊天区:token 内容流
- 侧栏/时间线:步骤与工具状态
这样用户既知道“模型在说什么”,也知道“系统正在做什么”。
29. 客户端工具状态机该如何设计?
参考答案:
至少覆盖以下状态:pending -> running -> success | error | denied | timeout。
同时要保证两点:
- 工具完成后必须回填 tool output
- 异常状态必须可见、可重试、可追踪
四、工具调用与安全治理
30. 工具调用(Function/Tool Calling)的标准流程是什么?
参考答案:
标准闭环是:模型产生 tool call -> 业务执行工具 -> 回填 tool result -> 模型继续推理直至产出最终答复。
关键是“回填闭环”必须完整,否则流程会卡住。
31. 为什么说工具 schema 设计比 Prompt 更决定稳定性?
参考答案:
因为 schema 是硬约束,Prompt 多数是软约束。
高质量 schema 通常具备:
- 明确字段语义与必填项
- 枚举和范围限制
additionalProperties: false
32. 什么情况下要禁用并行工具调用?
参考答案:
当工具存在顺序依赖、共享写入资源或副作用冲突时,应关闭并行。
典型例子:余额扣减、库存更新、同一订单状态流转。
33. ToolNode 的价值是什么?
参考答案:
ToolNode 可以统一承接工具请求、参数映射、执行回填和错误处理,降低手写 glue code 成本。
它的实际收益是:代码更短、行为更一致、排障路径更清晰。
34. 前端处理客户端工具调用最常见的坑是什么?
参考答案:
两个高频坑:
- 收到 tool call 后没回填 tool output,导致 Agent 挂起
- 只处理成功态,漏掉
error/denied/timeout等异常态
35. 面试里怎么回答“工具失败怎么办”?
参考答案:
可按三层回答:
- 用户层:可见错误、可重试、可降级
- Agent 层:错误信息回传模型,带重试/回退策略
- 系统层:结构化日志、告警、按 tool/租户聚合分析
36. 如何防止 Agent 误执行高风险工具?
参考答案:
建议采用“多闸门”策略:
- 动态工具白名单(按角色/场景/租户)
- 高风险工具强制 HITL
- 服务端参数级校验与二次鉴权
37. Prompt 注入导致工具越权,怎么防?
参考答案:
核心原则:不要把模型输出当最终权限决策。
防护组合:
- 工具权限在服务端判定
- 高危参数二次确认
- 对外部内容做来源隔离与最小信任
38. 为什么工具参数必须做服务端校验?
参考答案:
因为前端参数和模型参数都可能被污染。
服务端校验通常包括:
- 类型与范围验证
- 业务约束验证(资源归属、额度、状态)
- 幂等键与重放防护
39. 审计日志至少应记录哪些字段?
参考答案:
建议最小字段集:timestamp、thread_id、user_id、node、tool_name、tool_args_hash、result、latency、error_code。
这样才能满足排障、合规和追责需求。
40. 前端 Agent 面试中的安全追问一般怎么答?
参考答案:
推荐固定答题框架:
- 权限:谁能调用什么工具
- 校验:参数在哪一层兜底
- 审计:是否可追溯到线程与用户
- 兜底:高风险操作是否强制人工确认
五、持久化、可靠性与生产实践
41. Human-in-the-Loop(HITL)中的 interrupt 有什么作用?
参考答案:
interrupt 会在关键点暂停执行并持久化现场,等待人工输入后继续。
它适合高风险决策、审批流和低置信度场景的人工兜底。
42. Checkpointer(检查点)机制有什么作用?
参考答案:
Checkpointer 负责保存每一步状态快照,是恢复能力的基础。
它支持:
- 故障恢复
- 断点续跑
- 历史回放与调试
43. 持久化策略有哪些?适用场景是什么?
参考答案:
LangGraph.js 通过 Checkpointer Saver 包实现持久化:
代码示例:
// 1. MemorySaver - 本地开发/演示
import { MemorySaver } from "@langchain/langgraph";
const app = workflow.compile({ checkpointer: new MemorySaver() });
// 2. SqliteSaver - 单机持久化
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
const checkpointer = SqliteSaver.fromConnString("./checkpoints.db");
const app = workflow.compile({ checkpointer });
// 3. PostgresSaver - 生产环境
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const checkpointer = await PostgresSaver.fromConnString(process.env.DATABASE_URL);
const app = workflow.compile({ checkpointer });选型关键:会话时长、并发规模、合规要求、容灾需求。
注意事项:
MemorySaver进程重启即丢失,仅用于开发PostgresSaver需要先执行数据库迁移创建表结构- 生产环境建议配置 TTL 定期清理过期 checkpoint
PostgresSaver 生产环境设置:
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import { Pool } from "pg";
// 1. 数据库迁移(首次)
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await PostgresSaver.createTables(pool);
// 2. 创建 Checkpointer
const checkpointer = new PostgresSaver({ pool });
// 3. 编译图
const app = workflow.compile({ checkpointer });
// 4. 清理过期 checkpoint(可选)
await checkpointer.deleteCheckpoints({ before: Date.now() - 7 * 24 * 60 * 60 * 1000 });注意事项:
- 需要先执行
createTables创建 checkpoint 表结构 - 生产环境建议使用连接池管理(
pg.Pool) - 定期清理过期 checkpoint 以控制存储成本(建议保留 7-30 天)
- 支持
checkpoint_ns参数实现命名空间隔离
44. 生产环境为什么不能只用 MemorySaver?
参考答案:
MemorySaver 进程重启就丢状态,无法满足生产级恢复与审计要求。
一旦实例重启或扩缩容,会出现线程不可恢复、用户体验中断的问题。
45. interrupt 节点恢复时容易忽略什么行为?
参考答案:
恢复后节点会从头执行,所以 interrupt 之前的副作用操作必须幂等。
典型防线:幂等键、去重表、事务保护,避免重复下单/重复扣费。
46. 并发写入时如何保证状态一致性?
参考答案:
关键是为共享状态键设计确定性 reducer,并在关键写操作上做串行化或事务化。
避免“最后写入覆盖”导致结果不稳定。
47. 错误处理与回溯机制怎么设计?
参考答案:
建议显式设计错误节点与补偿路径,并记录重试计数、熔断状态。
排障时结合 checkpoint 回放,快速定位是“模型决策错”还是“工具执行错”。
48. 如何实现可重试与幂等性?
参考答案:
可重试不等于可重复执行,必须配套幂等设计。
实践要点:
- 外部副作用带幂等键
- 区分可重试错误和不可重试错误
- 从最近安全 checkpoint 重跑
49. 图结构变更会影响存量线程吗?
参考答案:
会。存量线程可能依赖旧节点和旧状态结构,直接切换新图会出现“找不到节点/字段不兼容”。
因此需要版本化管理和迁移策略。
50. 工作流版本管理与状态迁移如何做?
参考答案:
推荐策略:
- 图定义加版本号
- 新老版本并行运行
- 提供状态迁移函数并灰度切流
目标是避免一次性硬切导致线上中断。
51. 生产环境如何监控与调试?
参考答案:
核心指标至少包括:节点耗时、错误率、工具成功率、token 成本、恢复次数。
配合调用链追踪(如 LangSmith)可快速定位瓶颈节点和异常路径。
52. LangGraph.js 的性能优化策略有哪些?
参考答案:
优化通常从四个方向入手:
- 拆分重节点,减少串行阻塞
- 并行化可并行环节并加并发上限
- 缩减状态体积与无效上下文
- 降低高成本模型/工具调用频次
六、场景设计与代码实操
53. 实现最小聊天代理(含消息聚合)应包含哪些步骤?
参考答案:
最小可运行链路通常是:定义 messages state + reducer -> 构建输入/模型/输出节点 -> 编译图 -> 以样例对话验证多轮累积。
重点不是代码量,而是“状态是否稳定可复现”。
代码实现:
import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
// 1. 定义 State + Reducer (MessagesAnnotation 已内置)
const model = new ChatOpenAI({ model: "gpt-4" });
// 2. 构建节点
const chatNode = async (state: typeof MessagesAnnotation.State) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
};
// 3. 编译图
const workflow = new StateGraph(MessagesAnnotation)
.addNode("chat", chatNode)
.addEdge(START, "chat")
.addEdge("chat", END);
const app = workflow.compile();
// 4. 验证多轮累积
const result1 = await app.invoke({
messages: [{ role: "user", content: "Hi" }]
});
const result2 = await app.invoke({
messages: result1.messages.concat({ role: "user", content: "Bye" })
});
console.log(result2.messages); // [user:Hi, ai:Hello, user:Bye, ai:Goodbye]代码解析:
MessagesAnnotation自动提供messagesStateReducer,无需手动定义- 多轮对话通过手动拼接
messages数组实现状态累积 - 生产环境可配合
MemorySaver实现自动状态持久化
54. 实现“工具调用 + 中断审批”的 Agent 核心步骤是什么?
参考答案:
核心顺序:模型判定是否调用工具 -> 工具执行并回填 -> 高风险节点触发 interrupt -> 用户审批 -> Command({ resume }) 恢复。
每一步都要绑定同一 thread_id,并记录审计日志。
代码实现:
import { StateGraph, MessagesAnnotation, MemorySaver, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { interrupt, Command } from "@langchain/langgraph";
import { z } from "zod";
// 高风险工具
const deleteUserTool = new DynamicStructuredTool({
name: "delete_user",
description: "删除用户账户",
schema: z.object({ userId: z.string() }),
func: async ({ userId }) => `已删除用户 ${userId}`
});
const model = new ChatOpenAI().bindTools([deleteUserTool]);
// 工具执行节点(带审批)
const toolNode = async (state: typeof MessagesAnnotation.State) => {
const lastMessage = state.messages.at(-1);
const toolCall = lastMessage?.tool_calls?.[0];
if (toolCall?.name === "delete_user") {
const approval = await interrupt({ type: "approval", tool: toolCall });
if (approval !== "approved") {
return {
messages: [{
role: "tool",
content: "操作被拒绝",
tool_call_id: toolCall.id
}]
};
}
}
const result = await deleteUserTool.func(toolCall.args);
return {
messages: [{
role: "tool",
content: result,
tool_call_id: toolCall.id
}]
};
};
const workflow = new StateGraph(MessagesAnnotation)
.addNode("model", async (state) => ({
messages: [await model.invoke(state.messages)]
}))
.addNode("tools", toolNode)
.addConditionalEdges("model", (state) => {
return state.messages.at(-1)?.tool_calls?.length ? "tools" : END;
})
.addEdge("tools", "model");
const app = workflow.compile({ checkpointer: new MemorySaver() });
// 恢复执行
await app.invoke(
new Command({ resume: "approved" }),
{ configurable: { thread_id: "thread-123" } }
);关键流程:
- 模型判定是否调用工具 → 条件路由到
tools节点或END - 工具节点检测高风险操作 → 触发
interrupt并持久化状态 - 前端接收中断信号 → 渲染审批面板
- 用户审批后 → 使用
Command({ resume })恢复执行,传入审批结果 - 工具节点根据审批结果执行或拒绝,回填
tool消息 - 流程继续到模型节点 → 生成最终答复
审计要点:
- 每次
interrupt都会创建 checkpoint,记录审批请求时间 - 恢复时的
resume_value需要记录到审计日志 - 必须绑定
thread_id实现用户级追溯
55. 实现并行任务处理与结果聚合的关键点是什么?
参考答案:
入口节点扇出分支并行执行,在汇聚节点用 reducer 合并。
同时要设计:并发上限、超时策略、部分失败处理策略。
代码实现:
import { StateGraph, Annotation, START, END, Send } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
tasks: Annotation<string[]>(),
results: Annotation<string[]>({
reducer: (prev, curr) => prev.concat(curr),
default: () => []
})
});
// 扇出节点
function fanOut(state: typeof StateAnnotation.State) {
return state.tasks.map(task => new Send("worker", { task }));
}
// Worker 节点
const workerNode = async (state: { task: string }) => {
const result = await processTask(state.task);
return { results: [result] };
};
const workflow = new StateGraph(StateAnnotation)
.addNode("fanOut", fanOut)
.addNode("worker", workerNode)
.addNode("aggregate", (state) => ({
finalResult: state.results.join(", ")
}))
.addConditionalEdges(START, fanOut)
.addEdge("worker", "aggregate")
.addEdge("aggregate", END);关键点:
- 使用
Send实现动态扇出(每个任务一个 worker 实例) - 为共享状态键(
results)定义reducer合并并发结果 - 并发上限通过外部队列/信号量控制(LangGraph 本身不限制)
- 处理慢分支超时:在 worker 内部设置超时逻辑
56. 【场景题】用户反馈"有时会重复下单",如何定位和修复?
场景描述:
生产环境中,用户偶尔会收到重复订单确认邮件。日志显示同一 thread_id 的 createOrder 工具被调用了两次,但前端只发送了一次请求。
排查步骤:
// 1. 检查 checkpoint 历史
const history = await app.getStateHistory({
configurable: { thread_id: "order-123" }
});
for await (const state of history) {
console.log({
checkpoint_id: state.config.configurable.checkpoint_id,
next_nodes: state.next,
values: state.values
});
}
// 发现:createOrder 节点前有 interrupt,恢复后重新执行
// 2. 定位问题:interrupt 前的工具节点缺少幂等保护
const createOrderNode = async (state) => {
// ❌ 问题代码:每次执行都会创建新订单
const orderId = await database.createOrder(state.orderData);
return { orderId };
};
// 3. 修复:添加幂等键
const createOrderNode = async (state) => {
const idempotencyKey = `order:${state.threadId}:${state.checkpointId}`;
// ✅ 先检查是否已执行
const existing = await database.findOrderByIdempotencyKey(idempotencyKey);
if (existing) {
return { orderId: existing.id };
}
// 首次执行
const orderId = await database.createOrder({
...state.orderData,
idempotency_key: idempotencyKey
});
return { orderId };
};根因: interrupt 节点会保存状态,但恢复时会重新执行前面的节点。如果这些节点有副作用(如下单、扣费),必须做幂等保护。
预防措施:
- 所有副作用操作使用幂等键
- 在数据库层添加唯一约束
- 单元测试中模拟中断恢复场景
七、Agent 策略与优化
57. Agent 和 Workflow 的边界如何定义?
参考答案:
Workflow 适合确定性流程;Agent 适合动态决策。
工程上常见做法是“决策层 Agent + 执行层 Workflow”,把可预期步骤沉淀为固定流程。
58. Planner-Executor 模式的价值是什么?
参考答案:
把规划和执行拆开后,可解释性、可测试性、可替换性都会更好。
你可以独立优化 planner 策略,而不破坏 executor 的稳定执行逻辑。
59. 多 Agent 协作什么时候值得引入?
参考答案:
当任务天然可分工且单 Agent 上下文负担过重时才值得引入。
否则会增加通信成本、状态同步复杂度和故障面。
60. Agent 项目上线后如何持续优化?
参考答案:
建议按四维闭环推进:
- 效果:评测集回归与失败样本复盘
- 成本:token/工具开销优化
- 稳定:错误率与恢复能力提升
- 安全:越权率与人工接管策略优化
61. 生产环境如何优化以减少 Token 数量?
参考答案:
优先做“输入减法”,再做“调用减法”,最后做“输出减法”。
高性价比策略:
- 上下文裁剪:只保留当前任务相关历史,长会话做滚动摘要
- 检索压缩:RAG 先重排再压缩片段,限制每轮注入证据长度
- 模型路由:简单任务走小模型,复杂任务再升级大模型
- 提示词收敛:移除重复指令,系统提示模板化并复用
- 工具前置过滤:先规则判断,避免不必要的 LLM/tool 调用
- 缓存复用:对稳定问题启用语义缓存与结果缓存
- 输出约束:限定输出格式和最大长度,避免无效冗长回答
面试可落地回答:
- 先建立 token 分布基线(输入/输出/检索注入分别统计)
- 对高频路径做 A/B(如摘要窗口、top-k、max_tokens)
- 以“效果不降”为约束,持续压缩单次运行 token 成本
62. 如何回答“你如何评估 Agent 质量”?
参考答案:
推荐给出量化指标(对齐 LangSmith Observability):
核心指标:
task_success_rate: 端到端任务完成率tool_call_accuracy: 工具调用正确率first_token_latency: 首 token 延迟(P50/P95)total_tokens: Token 消耗量(输入 + 输出)cost_per_run: 单次运行成本(USD)
可靠性指标:
error_rate: 错误率(按节点/工具聚合)interrupt_rate: 人工接管率resume_success_rate: 中断恢复成功率checkpoint_write_latency: Checkpoint 写入延迟
安全指标:
unauthorized_tool_attempts: 越权工具调用尝试次数approval_rejection_rate: 审批拒绝率
LangSmith 集成示例:
import { LangChainTracer } from "langchain/callbacks";
const tracer = new LangChainTracer({
projectName: "production-agent",
metadata: {
environment: "prod",
version: "1.0.0"
}
});
const app = workflow.compile({ callbacks: [tracer] });八、LangChain 专项
63. LangGraph.js 与 LangChain.js 的关系和差异是什么?
参考答案:
LangChain.js 偏模型与工具生态封装;LangGraph.js 偏流程编排与状态运行时。
常见组合是:用 LangChain 提供模型/工具抽象,用 LangGraph 管理复杂执行流程。
64. 为什么 LangChain 生态要引入 LangGraph 的图式编排?
参考答案:
因为真实业务普遍存在分支、循环、人工介入和恢复需求,线性链路难以表达。
图式编排能显著提升可控性、可调试性和可审计性。
65. createAgent() 和 LangGraph 是什么关系?
参考答案:
在 LangChain JS 体系中,createAgent() 底层通常运行在 LangGraph 的图运行时之上。
也就是说,很多“看似简单的 Agent 调用”背后本质是图执行循环。
LangGraph.js 1.0 说明:
在 LangGraph.js 1.0 中,createAgent() 是正确的预构建 API(不是 createReactAgent)。它会自动创建包含工具调用循环的 StateGraph。
代码示例:
import { createAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { MemorySaver } from "@langchain/langgraph";
const model = new ChatOpenAI({ model: "gpt-4" });
const tools = [weatherTool, calculatorTool];
// createAgent 会自动创建包含工具调用循环的 StateGraph
const agent = createAgent({
llm: model,
tools,
checkpointer: new MemorySaver()
});
// 等价于手动构建以下 StateGraph:
// const workflow = new StateGraph(MessagesAnnotation)
// .addNode("model", modelNode)
// .addNode("tools", toolsNode)
// .addConditionalEdges("model", shouldContinue, {
// continue: "tools",
// end: END
// })
// .addEdge("tools", "model");优势:
- 快速启动,无需手动构建图结构
- 自动处理工具调用循环逻辑
- 支持 checkpointer、interrupt 等高级功能
何时手动构建 StateGraph:
- 需要自定义节点执行逻辑
- 需要复杂的条件路由(非标准工具调用循环)
- 需要多 Agent 协作
66. LangChain 的 Components 和 Chains 分别是什么?
参考答案:
Components 是可复用构建块(模型、提示词、检索器、解析器等);Chains 是把这些组件组合成可执行流程。
前者偏“零件”,后者偏“装配后的流水线”。
67. LangChain Agent 的执行闭环通常如何描述?
参考答案:
可概括为:接收任务 -> 推理决策 -> 调用工具 -> 接收反馈 -> 继续决策,直到满足停止条件。
面试里建议补一句:停止条件应显式化,避免无限循环。
68. LangChain 中 Embedding + Vector Store 的标准流程是什么?
参考答案:
离线:文档切块并向量化入库;在线:query 向量化后 top-k 检索,再与问题一起送入模型生成。
实践里通常会加 rerank、过滤和缓存来提升稳定性。
69. PromptTemplate 在工程里最常见的坑是什么?
参考答案:
最常见是模板变量和 input_variables 不一致,导致运行时缺参。
建议把模板变量做静态检查,并为关键模板补单测。
70. LangChain 如何实现多轮对话?
参考答案:
通常通过 memory 组件维护历史上下文,再注入后续调用。
为了控制成本和噪声,常采用“最近窗口 + 历史摘要”的混合策略。
71. MCP 和 Function Calling 的区别怎么答?
参考答案:
Function Calling 是模型输出函数调用的机制;MCP 是模型与外部工具系统之间的标准化协议。
前者回答“怎么调函数”,后者回答“如何统一接入工具生态”。
九、前端工程化专项
72. 如何实现 LangGraph Agent 的请求取消功能?
参考答案:
前端需要实现 AbortController 并传递给流式请求,后端监听取消信号。
代码实现:
// 前端实现
const abortController = new AbortController();
const stream = await fetch("/api/agent/stream", {
method: "POST",
body: JSON.stringify({ message: "Hello", thread_id: "123" }),
signal: abortController.signal
});
// 用户点击取消
cancelButton.onclick = () => abortController.abort();
// 后端处理(Next.js API Route)
export async function POST(req: Request) {
const signal = req.signal;
for await (const event of app.stream(input, { configurable: { thread_id } })) {
if (signal.aborted) {
console.log("Client cancelled request");
break;
}
// 发送事件
}
}关键点:
- 取消后 checkpoint 仍会保存最后一个完整节点的状态
- 下次请求可从中断点恢复
- 需要区分"用户取消"和"网络故障"
73. 流式连接断开后如何恢复?
参考答案:
依赖 thread_id + checkpoint 实现断点续传。
代码实现:
// 1. 前端检测断线
const eventSource = new EventSource(`/api/agent/stream?thread_id=${threadId}`);
eventSource.onerror = async () => {
console.log("Connection lost, reconnecting...");
eventSource.close();
// 2. 重新连接(自动从 checkpoint 恢复)
await reconnect(threadId);
};
// 3. 服务端自动恢复
async function reconnect(threadId: string) {
const latestState = await app.getState({ configurable: { thread_id: threadId } });
if (latestState.next.length > 0) {
// 还有未完成节点,继续执行
for await (const event of app.stream(null, { configurable: { thread_id: threadId } })) {
renderEvent(event);
}
}
}工程建议:
- 前端显示"连接中断,正在恢复..."状态
- 使用指数退避重连策略(1s → 2s → 4s → 8s)
- 记录断线时间,超时后提示用户手动刷新
74. 如何防止重复提交导致多次执行?
参考答案:
使用幂等键(Idempotency Key)+ 去重表。
代码实现:
// 前端生成幂等键
import { v4 as uuidv4 } from "uuid";
const idempotencyKey = uuidv4();
await fetch("/api/agent/invoke", {
method: "POST",
headers: {
"Idempotency-Key": idempotencyKey
},
body: JSON.stringify({ message: "Hello", thread_id: "123" })
});
// 服务端去重
const idempotencyCache = new Map(); // 生产用 Redis
export async function POST(req: Request) {
const key = req.headers.get("Idempotency-Key");
if (idempotencyCache.has(key)) {
return idempotencyCache.get(key); // 返回缓存结果
}
const result = await app.invoke(input, config);
idempotencyCache.set(key, result);
return result;
}防重关键:
- 前端每次新请求生成新的幂等键
- 服务端在执行前检查幂等键
- 幂等键有效期通常 24 小时
- 结合
thread_id双重校验
75. 多租户场景如何隔离 Agent 状态?
参考答案:
通过 thread_id 前缀 + 数据库行级隔离实现租户隔离。
代码实现:
// 1. thread_id 命名规范
function generateThreadId(tenantId: string, userId: string, sessionId: string) {
return `tenant:${tenantId}:user:${userId}:session:${sessionId}`;
}
// 2. 服务端校验租户权限
export async function POST(req: Request) {
const { thread_id } = await req.json();
const session = await getSession(req);
// 提取租户 ID
const [_, tenantId] = thread_id.split(":");
if (session.tenantId !== tenantId) {
return new Response("Forbidden", { status: 403 });
}
// 安全执行
const result = await app.invoke(input, { configurable: { thread_id } });
return result;
}
// 3. 数据库层隔离(Postgres Row-Level Security)
// CREATE POLICY tenant_isolation ON checkpoints
// USING (thread_id LIKE 'tenant:' || current_setting('app.tenant_id') || ':%');安全建议:
- 永远在服务端验证租户归属
- 使用数据库原生隔离机制
- 日志记录跨租户访问尝试
- 定期审计
thread_id命名合规性
76. 如何监控 Agent 执行的实时性能?
参考答案:
结合 LangSmith + 自定义 Metrics 收集节点级性能数据。
代码实现:
import { LangChainTracer } from "langchain/callbacks";
// 1. 启用 LangSmith
const tracer = new LangChainTracer({
projectName: "production-agent",
apiKey: process.env.LANGSMITH_API_KEY
});
const app = workflow.compile({
checkpointer,
callbacks: [tracer]
});
// 2. 自定义 Metrics
const metrics = {
nodeLatency: new Map<string, number[]>(),
toolSuccessRate: new Map<string, { success: number, total: number }>()
};
// 3. 在节点中收集指标
const monitoredNode = async (state) => {
const startTime = Date.now();
try {
const result = await originalNode(state);
const latency = Date.now() - startTime;
metrics.nodeLatency.get("nodeName")?.push(latency);
return result;
} catch (error) {
metrics.toolSuccessRate.get("nodeName").total++;
throw error;
}
};
// 4. 定期上报 (Prometheus/DataDog)
setInterval(() => {
const p95Latency = calculateP95(metrics.nodeLatency.get("nodeName"));
reportMetric("agent.node.latency.p95", p95Latency, { node: "nodeName" });
}, 60000);关键指标:
- 节点耗时 P50/P95/P99
- 工具调用成功率
- Token 消耗量
- Checkpoint 写入频率
- 中断恢复次数
77. 如何处理 Agent 长时间运行导致的前端超时?
参考答案:
采用"后台任务 + 轮询/WebSocket"模式。
代码实现:
// 1. 前端提交任务
const taskId = await fetch("/api/agent/submit", {
method: "POST",
body: JSON.stringify({ message: "复杂任务", thread_id: "123" })
}).then(r => r.json());
// 2. 后台执行(服务端)
export async function POST(req: Request) {
const { thread_id, message } = await req.json();
const taskId = uuidv4();
// 异步执行
executeInBackground(taskId, async () => {
await app.invoke({ messages: [message] }, { configurable: { thread_id } });
});
return Response.json({ taskId, status: "pending" });
}
// 3. 前端轮询状态
async function pollTaskStatus(taskId: string) {
const interval = setInterval(async () => {
const status = await fetch(`/api/agent/status/${taskId}`).then(r => r.json());
if (status.state === "completed") {
clearInterval(interval);
displayResult(status.result);
} else if (status.state === "failed") {
clearInterval(interval);
showError(status.error);
}
}, 2000);
}
// 4. 使用 WebSocket 实时推送(更优)
const ws = new WebSocket(`wss://api.example.com/agent/stream/${taskId}`);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
renderUpdate(update);
};适用场景:
- 执行时间 > 30 秒的任务
- 需要多步骤进度反馈
- 用户可能关闭页面后任务仍需继续
十、RAG 专项
78. 为什么 RAG 往往要外挂向量库或检索库?
参考答案:
模型参数知识存在时效和覆盖边界,外挂知识库可以低成本增量更新领域知识。
这样能提升事实性和可控性,并降低“纯靠提示词记忆”的不稳定性。
79. RAG 的典型链路怎么描述?
参考答案:
标准链路是:文档加载与切块 -> 向量化入库 -> query 向量化 -> 检索 top-k -> 构造提示 -> 生成答案 -> 引用与后处理。
面试时可强调“检索质量决定下游上限”。
80. RAG 的核心到底是什么?
参考答案:
核心不是“接入向量库”,而是“高质量上下文构建”。
重点包括:召回正确、重排有效、提示清晰、引用可信。
81. RAG Prompt 模板设计要点有哪些?
参考答案:
应明确:角色、证据边界、输出格式、无答案回退策略。
常用约束是“仅基于给定证据回答;证据不足时明确拒答或澄清”。
82. 如何评价一个 RAG 系统效果?
参考答案:
至少分两层评估:
- 检索层:命中率、召回率、排序质量
- 生成层:事实一致性、引用正确率、答案完整性
83. 通用 RAG 的常见改进手段有哪些?
参考答案:
常见优化:查询改写(MultiQuery/HyDE)、混合检索(向量+关键词)、重排序、缓存、分层索引。
落地时建议“先评测后优化”,避免盲目堆技术。
84. 文档切割策略为什么影响很大?
参考答案:
切太碎会丢语义,切太大又会引入噪声和成本。
应在语义完整性、chunk 长度、重叠窗口之间平衡,并用离线评测集持续调参。
85. 重排序(Rerank)在 RAG 中的作用是什么?
参考答案:
Rerank 在粗召回后做二次排序,提升最终送入模型的上下文质量。
它通常直接影响答案准确率,尤其在候选文档较多时。
86. 生产 RAG 常见失败模式有哪些?
参考答案:
高频失败包括:召回错误、召回不足、上下文冲突、引用错配、长文截断。
治理手段是“日志 + 评测集 + 指标看板”形成闭环。
87. 语义切割怎么做?
参考答案:
可按语义边界(段落/主题/句法)先粗分,再按长度与相似度合并或拆分。
目标是让每个 chunk 既可独立检索,又不丢关键上下文。
88. 向量数据库索引算法常见有哪些?如何选?
参考答案:
常见有 HNSW、IVF/PQ、Flat。它们在精度、速度、内存占用上各有权衡。
选型要结合:数据规模、延迟目标、硬件预算、可接受召回损失。
89. 嵌入模型与索引算法是什么关系?
参考答案:
嵌入模型决定向量空间质量,索引算法决定检索效率与近似精度。
两者需要联调:上游 embedding 质量差,索引再强也很难救。
90. 如何设计一个 SELF-RAG 的 LangGraph 工作流?
参考答案:
可按“检索 -> 相关性判断 -> query 重写/继续检索 -> 生成 -> 引用校验”建图。
关键是给出可迭代回路和退出条件,避免无限重试。
SELF-RAG 工作流程图:
LangGraph 实现要点:
- 相关性判断节点返回
"rewrite"或"generate" - 引用校验节点返回
"end"或"retrieve" - 必须设置
recursionLimit(建议 5-10)防止无限循环
十一、项目实战(Chat Bot)
91. 你们项目里的会话记忆是怎么实现的?thread_id 起什么作用?
参考答案:
我们把“记忆”拆成两层:
1)线程内短期记忆:由 LangGraph checkpointer 持久化(项目里是 SupabaseSaver);
2)会话元数据:由业务表保存会话信息(如标题、用户归属、更新时间)。
每次 invoke/stream 都带 configurable.thread_id。LangGraph 会把这个线程对应的状态快照(messages、next、metadata)自动关联起来。这样前端刷新页面后,只要用同一个 thread_id 继续请求,就能“接着聊”。
thread_id 在工程上是关键主键:
- 会话隔离:A 会话的状态不会污染 B 会话
- 恢复执行:中断恢复、失败重试都依赖同一线程上下文
- 可审计:可以按
thread_id回看状态演进和节点轨迹 - 多端一致:Web 端和移动端都能挂载同一线程
面试里可以补一句:如果 thread_id 设计不稳定(比如每次请求随机生成),系统就会退化成“无记忆单轮问答”。
92. 流式输出链路里,前后端分别负责什么?
参考答案:
后端职责是“稳定产流”,前端职责是“平滑消费”。
后端侧:
- 用 LangGraph 的
streamEvents获取模型与工具执行事件 - 把事件编码后通过 HTTP 流持续推送(通常是 chunked/SSE 风格)
- 只做传输和协议封装,不在路由层塞复杂业务逻辑
前端侧:
- 用
ReadableStream读取增量分片 - 做“半包缓冲”:一个 JSON 被拆成两段时先缓存,凑齐再解析
- 将 token 增量合并到当前消息,避免全量替换导致光标抖动或卡顿
稳定性关键点:
- 节流渲染:高频 token 更新不要每个字符都触发重排
- 断流兜底:网络中断后给出可恢复状态,不要让 UI 卡死
- 事件分层:文本增量、工具调用、错误事件分别处理,避免耦合
93. 为什么要做“统一工具配置系统”,它解决了什么问题?
参考答案:
最初工具系统容易出现三类问题:
1)定义散落在不同目录;2)参数格式不统一;3)启停逻辑分散在前后端多个分支。
统一配置系统就是把“声明、装配、注入”三件事收敛到一处。
典型做法:
- 定义统一
ToolConfig:名称、描述、参数 schema、工具类型、是否启用 - 运行时根据会话/用户选择动态组装工具列表
- 用 Zod 做参数约束,减少模型生成脏参数导致的执行失败
工程收益:
- 扩展成本低:新增工具基本是“加配置 + 实现 handler”
- 可治理:可以做灰度开关、权限控制、审计统计
- 可测试:工具装配逻辑可以单测,不必依赖整条 Agent 链路
面试里可以强调:统一配置不是“代码整理”,而是把工具能力从“硬编码”升级为“可运营能力”。
94. 你们是如何做多模型提供商兼容的?
参考答案:
我们在后端做了“模型适配层”,核心思想是统一输入、屏蔽差异。
统一约定:
- 前端只传模型标识(如
provider:modelName)和通用参数(temperature、maxTokens) - 适配层负责把通用参数映射到各厂商 SDK 的实际字段
差异屏蔽点:
- 鉴权方式不同(API Key、Base URL)
- 响应格式不同(文本块结构、工具调用字段)
- 能力边界不同(是否支持多模态、是否支持特定工具协议)
为什么这样设计:
- 前端无需感知厂商变化,避免 UI 层频繁改动
- 便于做 fallback:主模型失败可切备用模型
- 可做成本策略:简单问题走低价模型,复杂问题走高质量模型
95. 介绍一下你们的 Canvas Artifacts 协议和执行链路。
参考答案:
Canvas Artifacts 的目标是把“AI 生成代码”做成可控产品能力,而不是让模型直接输出一坨文本。
协议层:
- 约束模型输出
<canvasArtifact>、<canvasCode>等标签 - 明确 artifact 的
id/type/title和 code 的language - 这样能在流式场景下稳定识别“哪里是元信息,哪里是代码体”
解析层:
- 使用状态机做增量解析,支持 chunk 到达即处理
- 即使标签跨 chunk 被拆开,也能通过缓存拼接继续解析
- 解析结果进入 Artifact Store,维护
creating/streaming/ready/error等生命周期
渲染层:
- UI 提供“代码/预览”切换
- 用户修改代码后可再次执行预览
- 支持版本化思路(至少保留当前版本和失败版本上下文)
这套设计的价值在于:把“模型不稳定输出”变成“协议化输入”,前端可预测、可治理。
96. 为什么 Canvas 需要沙箱执行?你们怎么处理错误?
参考答案:
AI 代码默认是不可信输入,直接在主应用执行会有三类风险:
1)安全风险:脚本可访问主页面上下文;
2)稳定性风险:运行时异常拖垮主应用;
3)可维护性风险:错误边界不清晰,难排障。
所以我们把执行放到沙箱(如 iframe sandbox 方案):
- 权限最小化:限制脚本能力与跨域访问
- 环境隔离:即使崩溃也只影响预览容器
- 通信可控:通过
postMessage上报状态和错误
错误治理流程:
- 编译错误:高亮错误位置与信息,阻止进入“ready”态
- 运行错误:捕获异常并回传 UI,保留上一个可用预览
- 恢复机制:用户可编辑重试,必要时调用 AI 辅助修复
面试可加分点:强调“沙箱不是可选优化,而是动态代码产品化的安全底线”。
97. 自定义渲染协议(非 Canvas)在项目中的作用是什么?
参考答案:
非 Canvas 场景下,自定义渲染协议的核心价值是“把回答从文本升级为交互组件”。
例如图片卡片、视频卡片、结构化信息块,都可以通过协议驱动渲染。
为什么不用纯 Markdown:
- Markdown 对复杂交互表达弱
- 组件参数不容易做强类型约束
- 流式解析时很难稳定识别复杂结构
协议化后的收益:
- 输出可控:模型按指定标签生成,减少解析歧义
- 渲染一致:前端用组件映射表统一渲染标准
- 易扩展:新增组件只需要扩展协议类型和映射关系
落地关键点:
- 标签要有命名空间,避免和业务文本冲突
- 解析器必须容错(缺标签、错属性、跨 chunk)
- 未知类型要优雅降级,避免整条消息渲染失败
98. Route → Service → Data Access 三层架构在这个项目里具体解决了哪些工程问题?
参考答案:
DeepResearch 本质是“长链路复合任务”,不是一次问答。它通常至少包含:需求澄清、计划生成、资料检索、分主题研究、汇总成稿、人工修订。
单次 LLM 调用的问题:
- 过程不可见:不知道哪一步出错
- 难以干预:用户无法在中间改计划
- 难恢复:中途失败往往要整轮重来
LangGraph 状态机方案:
- 把流程拆成多个节点,每个节点职责单一
- 用条件边控制流转,失败可回到指定节点重试
- 用
interrupt做计划审批、人机协作 - 用并行能力处理多章节研究,缩短总耗时
所以选状态机不是“技术炫技”,而是为了得到可控性、可观测性和可恢复性这三项工程能力。
99. DeepResearch 为什么要用 LangGraph 状态机而不是单次大模型调用?
参考答案:
这套三层架构本质上是在解决 AI 应用里最常见的“路由膨胀、逻辑耦合、难测试、难替换”问题。
在项目中,三层职责是明确分开的:
- Route 层:只做请求解析、参数校验、响应封装,不承载复杂业务
- Service 层:承载核心业务流程(模型路由、工具装配、状态流转)
- Data Access 层:统一数据库/存储读写,屏蔽底层实现细节
它具体解决了这些工程问题:
- 可维护性问题:避免把 LangGraph 调度、工具调用、SQL 都塞在一个 API 文件里
- 可测试性问题:Service 和 Data 层可以单测,路由层只需做轻量集成测试
- 可扩展性问题:替换存储、增加模型提供商、引入新工具时,改动范围可控
- 协作效率问题:前端/后端多人并行时,边界清晰,冲突更少
在你的项目场景里,这个分层尤其重要,因为同时存在:
- 流式传输链路(Route 负责流输出协议)
- Agent 编排逻辑(Service 负责工作流和工具调用)
- 持久化与权限边界(Data 层负责会话/状态存储访问)
面试里可以总结为一句话:
三层架构不是“分文件夹”,而是把传输、业务、数据访问三种变化频率不同的代码解耦,降低复杂 AI 应用的演进成本。
100. 如果让你继续演进这个项目,优先会做哪三件事?
参考答案:
我会按“先稳态、再提效、后扩展”的顺序做三件事:
1)可观测性完善
- 补齐节点级指标:P50/P95 延迟、错误率、工具调用成功率、恢复成功率
- 打通 trace 到具体
thread_id,支持问题回放 - 建立告警阈值,先发现再优化
2)可靠性增强
- 对副作用节点(写库、外部调用)加幂等键
- 失败路径做分级重试(瞬时错误重试,逻辑错误快速失败)
- 完善超时与熔断策略,防止局部故障拖垮全链路
3)测试与发布质量
- 给协议解析器、工具装配器、DeepResearch 路由加回归测试
- 增加端到端场景(流式、断流恢复、人工中断恢复)
- 结合灰度发布验证稳定性,减少一次性全量风险
这三项的共同目标是:把项目从“能跑”推进到“可长期稳定运营”。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。