节点设计
深入探讨节点的类型、编写规范以及异步和配置化节点的实现
📚 学习目标
学完这篇文章后,你将能够:
- 编写标准的同步及异步节点函数
- 使用
RunnableConfig编写可配置的节点 - 理解节点的输入输出规范
- 掌握节点内的常用错误处理方式
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- Async/Await 异步编程模式
- JavaScript 异常处理 (try/catch)
引言
在 LangGraphJS 中,节点(Nodes) 是图的执行单元,负责处理具体的业务逻辑。如果你熟悉前端开发,可以将节点理解为:
- React 组件的 render 函数:接收 props(状态),返回新的 JSX(状态更新)
- Redux 的 Action Creator:接收当前状态,返回 action(状态变更)
- Vue 的计算属性:基于响应式数据进行计算和转换
- 函数式编程的纯函数:输入确定,输出确定,无副作用
节点是 LangGraph 应用的"工作者",它们读取状态、执行逻辑、并产生新的状态更新。
💡 与前端开发的类比
- 节点函数 ≈ React 组件函数 + 事件处理器
- 状态参数 ≈ React Props + Context
- 配置参数 ≈ React Context + 环境变量
- 返回值 ≈ setState() 的参数 + dispatch(action)
节点的核心特征
🔧 函数本质
- 节点本质上是 JavaScript/TypeScript 函数
- 第一个参数是当前状态
- 第二个参数是可选的配置信息
- 返回状态更新对象或 Promise
📥 输入规范
type NodeFunction = (
state: StateType, // 当前状态
config?: RunnableConfig, // 可选配置
) => StateUpdate | Promise<StateUpdate>;📤 输出规范
- 返回部分状态更新对象
- 可以返回 Promise(异步节点)
- 可以返回 Command 对象(控制流)
- 空对象
{}表示不更新状态
1 节点函数规范
在 LangGraph 中,节点就是一个简单的函数。
基本签名
import { RunnableConfig } from '@langchain/core/runnables';
// state: 当前状态
// config: 运行时配置(可选)
const myNode = async (state: State, config?: RunnableConfig) => {
// 业务逻辑
const result = await doSomething(state.input);
// 返回状态更新
return {
output: result,
};
};🎨 可视化说明
下面的图表展示了节点在 LangGraph 中的工作流程:
图表说明:
- 输入状态:节点接收当前的完整状态
- 配置参数:可选的运行时配置
- 业务逻辑:节点内部的处理流程
- 状态更新:节点返回的增量更新
2 节点类型详解
1. 异步节点(最常用)
绝大多数涉及 LLM 调用、数据库查询、API 请求的节点都是异步的。
const llmNode = async (state: typeof StateAnnotation.State) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
};2. 同步节点
用于纯逻辑处理,如数据格式化、简单计算。
const formatNode = (state: typeof StateAnnotation.State) => {
return { content: state.content.trim() };
};3. 可配置节点
通过 config 参数,让节点行为在运行时可调整。这在多用户多配置场景下非常有用。
const configurableNode = async (state: State, config?: RunnableConfig) => {
// 从配置中读取参数,默认为 'gpt-3.5-turbo'
const modelName = config?.configurable?.modelName || 'gpt-3.5-turbo';
const model = new ChatOpenAI({ model: modelName });
// ...
};调用时传入配置:
await graph.invoke(inputs, {
configurable: { modelName: 'gpt-4' },
});ℹ️ 可配置节点的优势
使用 RunnableConfig 可以在不修改节点代码的情况下调整行为,这对于多租户系统、A/B 测试和动态配置非常有用。
🎯 节点设计最佳实践
1. 单一职责原则
每个节点应该只做一件事,并把它做好。
// ✅ 好的设计:职责单一
const validateInputNode = (state) => {
const errors = [];
if (!state.userId) errors.push('缺少用户ID');
if (!state.query) errors.push('缺少查询内容');
return { validationErrors: errors };
};
const fetchDataNode = async (state) => {
const data = await database.query(state.query);
return { queryResults: data };
};
// ❌ 不好的设计:职责混杂
const validateAndFetchNode = async (state) => {
// 验证 + 获取数据混在一起
if (!state.userId) throw new Error('Invalid user');
const data = await database.query(state.query);
return { queryResults: data };
};2. 错误处理模式
在节点内妥善处理错误,避免整个图崩溃。
const safeAPINode = async (state) => {
try {
const response = await fetch(state.apiUrl);
const data = await response.json();
return { apiData: data, error: null };
} catch (error) {
console.error('API 调用失败:', error);
return {
apiData: null,
error: error.message,
retryCount: (state.retryCount || 0) + 1,
};
}
};3. 保持纯函数特性
尽量避免副作用,使节点易于测试和调试。
// ✅ 好的设计:纯函数
const processDataNode = (state) => {
const processed = state.rawData.map((item) => ({
...item,
processed: true,
timestamp: Date.now(),
}));
return { processedData: processed };
};
// ❌ 不好的设计:有副作用
const badNode = (state) => {
// 直接修改输入状态(副作用)
state.rawData.forEach((item) => {
item.processed = true;
});
return { processedData: state.rawData };
};3 错误处理
在节点内部处理错误,可以防止整个图崩溃,并允许实现重试或降级逻辑。
推荐模式
const safeToolNode = async (state: State) => {
try {
const result = await tool.invoke(state.query);
return { toolResult: result, error: null };
} catch (e) {
console.error('Tool execution failed', e);
// 返回错误状态,而不是抛出异常
return {
error: e.message,
// 可选:触发重试逻辑
retryCount: state.retryCount + 1,
};
}
};4 节点怎么拆:把复杂逻辑拆成可测试的“小块”
一个很常见的反模式是:
- 一个节点里同时做“解析输入 -> 检索 -> 生成 -> 格式化 -> 记录指标”
结果就是:难测、难改、难复用。
更好的做法是按“职责”拆节点:
拆分之后,你可以:
- 对
parse写单元测试(纯函数) - 对
search做超时/重试 - 对
answer做模型切换与 token 限制
5 特殊节点:START 和 END
- START:虽然代码中常用
addEdge(START, 'node'),但 START 本身不是一个你需要编写函数的节点,它只是一个标记,代表图的入口。 - END:同样是标记,代表流程结束。
import { START, END } from '@langchain/langgraph';
graph.addEdge(START, 'entryNode');
graph.addEdge('finalNode', END);6 可测试性:把节点当成普通函数测
节点的本质是函数,所以测试成本很低。你可以先从“纯逻辑节点”开始(不依赖网络/LLM)。
import { describe, it, expect } from 'vitest';
type State = { input: string; output?: string };
const trimNode = (state: State) => ({ output: state.input.trim() });
describe('trimNode', () => {
it('trims whitespace', () => {
expect(trimNode({ input: ' hi ' }).output).toBe('hi');
});
});这会让你在扩展图结构时更有信心(尤其是多节点、多分支)。
💡 练习题
1. 改造题
将一个硬编码了 API Key 的节点函数,改造成从 RunnableConfig 中读取 Key 的可配置节点。
点击查看答案
// ❌ 硬编码版本
const oldNode = async (state) => {
const apiKey = 'sk-hardcoded-key'; // 硬编码
const model = new ChatOpenAI({ apiKey });
// ...
};
// ✅ 可配置版本
const configurableNode = async (state, config?: RunnableConfig) => {
const apiKey = config?.configurable?.apiKey || process.env.OPENAI_API_KEY;
const model = new ChatOpenAI({ apiKey });
// ...
};
// 使用时
await graph.invoke(state, {
configurable: { apiKey: 'sk-user-specific-key' },
});2. 设计题
设计一个"网络请求节点",要求包含超时处理(例如 5 秒超时)和简单的重试机制(如果失败,检查重试计数)。
点击查看答案
const networkRequestNode = async (state, config?: RunnableConfig) => {
const maxRetries = 3;
const timeout = config?.configurable?.timeout || 5000;
const retryCount = state.retryCount || 0;
try {
// 使用 Promise.race 实现超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await Promise.race([
fetch(state.apiUrl, { signal: controller.signal }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout),
),
]);
clearTimeout(timeoutId);
const data = await response.json();
return {
data,
error: null,
retryCount: 0, // 成功后重置
};
} catch (error) {
console.error(`请求失败 (尝试 ${retryCount + 1}/${maxRetries}):`, error);
if (retryCount < maxRetries) {
return {
error: error.message,
retryCount: retryCount + 1,
shouldRetry: true,
};
} else {
return {
error: `最终失败: ${error.message}`,
retryCount: retryCount + 1,
shouldRetry: false,
};
}
}
};✅ 总结
本章要点:
- 节点本质上是
(State, Config?) => Partial<State>的函数 - 节点是 LangGraph 应用的执行单元,负责具体的业务逻辑
- 充分利用
async/await处理 I/O 操作(LLM 调用、数据库查询等) - 使用
RunnableConfig提升节点的复用性和灵活性 - 优雅的错误处理是构建健壮 Agent 的关键
最佳实践:
- 遵循单一职责原则,每个节点只做一件事
- 保持节点的纯函数特性,避免副作用
- 在节点内部处理错误,返回错误状态而非抛出异常
- 将复杂逻辑拆分成多个小节点,提高可测试性和可维护性
- 利用 TypeScript 类型系统确保状态更新的类型安全
下一步:节点只是孤岛,让我们通过**边(Edges)**将它们连接起来,形成完整的执行流程。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。