核心组件详解

节点设计

深入探讨节点的类型、编写规范以及异步和配置化节点的实现

📚 学习目标

学完这篇文章后,你将能够:

  • 编写标准的同步及异步节点函数
  • 使用 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)**将它们连接起来,形成完整的执行流程。

登录以继续阅读

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

立即登录

On this page