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(检查点持久化,如 MemorySaverSqliteSaverPostgresSaver
  • interrupt() / Command(人机中断与恢复)
  • .compile()(图编译与配置)

5. 在 LangGraph.js 中,messages 字段为什么要配置 reducer(如 messagesStateReducer)?它如何处理“追加”和“更新”?

参考答案:

messagesStateReducermessages 字段的状态合并规则,核心目标是“安全地维护消息历史”。

它的行为可以概括为两点:

  • 追加:新消息默认追加到已有消息列表,而不是覆盖整个历史。
  • 更新:若新旧消息 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. 审计日志至少应记录哪些字段?

参考答案:

建议最小字段集:timestampthread_iduser_idnodetool_nametool_args_hashresultlatencyerror_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" } }
);

关键流程:

  1. 模型判定是否调用工具 → 条件路由到 tools 节点或 END
  2. 工具节点检测高风险操作 → 触发 interrupt 并持久化状态
  3. 前端接收中断信号 → 渲染审批面板
  4. 用户审批后 → 使用 Command({ resume }) 恢复执行,传入审批结果
  5. 工具节点根据审批结果执行或拒绝,回填 tool 消息
  6. 流程继续到模型节点 → 生成最终答复

审计要点

  • 每次 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_idcreateOrder 工具被调用了两次,但前端只发送了一次请求。

排查步骤:

// 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 路由加回归测试
  • 增加端到端场景(流式、断流恢复、人工中断恢复)
  • 结合灰度发布验证稳定性,减少一次性全量风险

这三项的共同目标是:把项目从“能跑”推进到“可长期稳定运营”。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page

一、LangGraph 基础概念1. LangGraph.js 是什么?它解决了什么问题?2. StateGraph 和 State 的区别是什么?3. 什么是 Node 和 Edge?4. LangGraph.js 的核心组件有哪些?5. 在 LangGraph.js 中,messages 字段为什么要配置 reducer(如 messagesStateReducer)?它如何处理“追加”和“更新”?6. 什么是 thread_id?为什么它很关键?7. 什么是 checkpoint8. 短期记忆和长期记忆有什么区别?9. LangGraph.js 与 LCEL 相比有什么优势?10. LangGraph.js 相比通用工作流框架(如 Temporal)有什么特点?二、控制流与运行时11. 如何实现条件分支(Conditional Edges)?12. 如何实现循环(Loop)?13. 如何实现并行执行(Parallel Execution)?14. 什么是 Super-step?15. Orchestrator-Worker(扇出/扇入)模式如何建模?16. 如何限制并行分支的并发数?17. LangGraph 节点是如何工作的?18. Pregel Runtime 在 LangGraph 中负责什么?19. LangGraph 的 Time Travel(时间旅行)能力是什么?三、前端 Agent 架构与流式交互20. 前端 Agent 为什么推荐“前端 UI + 后端 Runtime”分层?21. 为什么说“Agent 不是前端组件,而是状态机/工作流系统”?22. 前端只做聊天窗口会有哪些短板?23. 如何设计“可中断审批”的前端体验?24. 为什么要区分“服务端工具”和“客户端工具”?25. LangGraph.js 常见 streamMode 有哪些?26. 什么时候用 updates,什么时候用 messages27. 前端如何避免流式渲染性能抖动?28. 前端如何同时展示“内容流”和“步骤流”?29. 客户端工具状态机该如何设计?四、工具调用与安全治理30. 工具调用(Function/Tool Calling)的标准流程是什么?31. 为什么说工具 schema 设计比 Prompt 更决定稳定性?32. 什么情况下要禁用并行工具调用?33. ToolNode 的价值是什么?34. 前端处理客户端工具调用最常见的坑是什么?35. 面试里怎么回答“工具失败怎么办”?36. 如何防止 Agent 误执行高风险工具?37. Prompt 注入导致工具越权,怎么防?38. 为什么工具参数必须做服务端校验?39. 审计日志至少应记录哪些字段?40. 前端 Agent 面试中的安全追问一般怎么答?五、持久化、可靠性与生产实践41. Human-in-the-Loop(HITL)中的 interrupt 有什么作用?42. Checkpointer(检查点)机制有什么作用?43. 持久化策略有哪些?适用场景是什么?44. 生产环境为什么不能只用 MemorySaver45. interrupt 节点恢复时容易忽略什么行为?46. 并发写入时如何保证状态一致性?47. 错误处理与回溯机制怎么设计?48. 如何实现可重试与幂等性?49. 图结构变更会影响存量线程吗?50. 工作流版本管理与状态迁移如何做?51. 生产环境如何监控与调试?52. LangGraph.js 的性能优化策略有哪些?六、场景设计与代码实操53. 实现最小聊天代理(含消息聚合)应包含哪些步骤?54. 实现“工具调用 + 中断审批”的 Agent 核心步骤是什么?55. 实现并行任务处理与结果聚合的关键点是什么?56. 【场景题】用户反馈"有时会重复下单",如何定位和修复?七、Agent 策略与优化57. Agent 和 Workflow 的边界如何定义?58. Planner-Executor 模式的价值是什么?59. 多 Agent 协作什么时候值得引入?60. Agent 项目上线后如何持续优化?61. 生产环境如何优化以减少 Token 数量?62. 如何回答“你如何评估 Agent 质量”?八、LangChain 专项63. LangGraph.js 与 LangChain.js 的关系和差异是什么?64. 为什么 LangChain 生态要引入 LangGraph 的图式编排?65. createAgent() 和 LangGraph 是什么关系?66. LangChain 的 Components 和 Chains 分别是什么?67. LangChain Agent 的执行闭环通常如何描述?68. LangChain 中 Embedding + Vector Store 的标准流程是什么?69. PromptTemplate 在工程里最常见的坑是什么?70. LangChain 如何实现多轮对话?71. MCP 和 Function Calling 的区别怎么答?九、前端工程化专项72. 如何实现 LangGraph Agent 的请求取消功能?73. 流式连接断开后如何恢复?74. 如何防止重复提交导致多次执行?75. 多租户场景如何隔离 Agent 状态?76. 如何监控 Agent 执行的实时性能?77. 如何处理 Agent 长时间运行导致的前端超时?十、RAG 专项78. 为什么 RAG 往往要外挂向量库或检索库?79. RAG 的典型链路怎么描述?80. RAG 的核心到底是什么?81. RAG Prompt 模板设计要点有哪些?82. 如何评价一个 RAG 系统效果?83. 通用 RAG 的常见改进手段有哪些?84. 文档切割策略为什么影响很大?85. 重排序(Rerank)在 RAG 中的作用是什么?86. 生产 RAG 常见失败模式有哪些?87. 语义切割怎么做?88. 向量数据库索引算法常见有哪些?如何选?89. 嵌入模型与索引算法是什么关系?90. 如何设计一个 SELF-RAG 的 LangGraph 工作流?十一、项目实战(Chat Bot)91. 你们项目里的会话记忆是怎么实现的?thread_id 起什么作用?92. 流式输出链路里,前后端分别负责什么?93. 为什么要做“统一工具配置系统”,它解决了什么问题?94. 你们是如何做多模型提供商兼容的?95. 介绍一下你们的 Canvas Artifacts 协议和执行链路。96. 为什么 Canvas 需要沙箱执行?你们怎么处理错误?97. 自定义渲染协议(非 Canvas)在项目中的作用是什么?98. Route → Service → Data Access 三层架构在这个项目里具体解决了哪些工程问题?99. DeepResearch 为什么要用 LangGraph 状态机而不是单次大模型调用?100. 如果让你继续演进这个项目,优先会做哪三件事?