【LangGraph】Integrate LangGraph into a React app:将 LangGraph 集成到你的 React 应用 示例

如何将 LangGraph 集成到你的 React 应用(How to integrate LangGraph into your React application)

预备知识

useStream() React 钩子提供了一种无缝的方式,将 LangGraph 集成到你的 React 应用中。它处理了流式传输、状态管理和分支逻辑的所有复杂性,让你可以专注于构建出色的聊天体验。

主要功能:

  • 消息流式传输:处理消息片段流以形成完整消息
  • 自动状态管理:管理消息、中断、加载状态和错误
  • 对话分支:从聊天历史的任意点创建替代对话路径
  • 与 UI 无关的设计:使用你自己的组件和样式

让我们探索如何在你的 React 应用中使用 useStream()

useStream() 为创建定制的聊天体验提供了坚实的基础。对于预构建的聊天组件和界面,我们还推荐查看 CopilotKitassistant-ui

安装

npm install @langchain/langgraph-sdk @langchain/core

示例

"use client";

import { useStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";

export default function App() {
  const thread = useStream<{ messages: Message[] }>({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    messagesKey: "messages",
  });

  return (
    <div>
      <div>
        {thread.messages.map((message) => (
          <div key={message.id}>{message.content as string}</div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();

          const form = e.target as HTMLFormElement;
          const message = new FormData(form).get("message") as string;

          form.reset();
          thread.submit({ messages: [{ type: "human", content: message }] });
        }}
      >
        <input type="text" name="message" />

        {thread.isLoading ? (
          <button key="stop" type="button" onClick={() => thread.stop()}>
            Stop
          </button>
        ) : (
          <button key="submit" type="submit">Send</button>
        )}
      </form>
    </div>
  );
}

自定义你的 UI

useStream() 钩子在后台处理所有复杂的状态管理,为你提供简单的接口来构建 UI。以下是你开箱即用的功能:

  • 线程状态管理
  • 加载和错误状态
  • 中断
  • 消息处理和更新
  • 分支支持

以下是如何有效使用这些功能的示例:

加载状态

isLoading 属性告诉你何时有活跃的流,使你能够:

  • 显示加载指示器
  • 在处理期间禁用输入字段
  • 显示取消按钮
export default function App() {
  const { isLoading, stop } = useStream<{ messages: Message[] }>({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    messagesKey: "messages",
  });

  return (
    <form>
      {isLoading && (
        <button key="stop" type="button" onClick={() => stop()}>
          Stop
        </button>
      )}
    </form>
  );
}

线程管理

通过内置的线程管理跟踪对话。你可以访问当前线程 ID,并在创建新线程时收到通知:

const [threadId, setThreadId] = useState<string | null>(null);

const thread = useStream<{ messages: Message[] }>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",

  threadId: threadId,
  onThreadId: setThreadId,
});

我们建议将 threadId 存储在 URL 的查询参数中,以便用户在页面刷新后可以恢复对话。

消息处理

useStream() 钩子会跟踪从服务器接收的消息片段,并将它们拼接在一起形成完整消息。完成的消息片段可以通过 messages 属性检索。

默认情况下,messagesKey 设置为 messages,新消息片段将追加到 values["messages"] 中。如果你的消息存储在不同的键下,可以更改 messagesKey 的值。

import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";

export default function HomePage() {
  const thread = useStream<{ messages: Message[] }>({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    messagesKey: "messages",
  });

  return (
    <div>
      {thread.messages.map((message) => (
        <div key={message.id}>{message.content as string}</div>
      ))}
    </div>
  );
}

在底层,useStream() 钩子使用 streamMode: "messages-tuple" 从图节点内的任何 LangChain 聊天模型调用接收消息流(即单个 LLM 令牌)。了解更多关于消息流式传输的信息,请参见流式传输指南

中断

useStream() 钩子暴露了 interrupt 属性,其中包含线程的最后一次中断。你可以使用中断来:

  • 在执行节点之前渲染确认 UI
  • 等待用户输入,允许代理向用户提出澄清问题

了解更多关于中断的信息,请参见如何处理中断指南

const thread = useStream<{ messages: Message[] }, { InterruptType: string }>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  messagesKey: "messages",
});

if (thread.interrupt) {
  return (
    <div>
      已中断!{thread.interrupt.value}
      <button
        type="button"
        onClick={() => {
          // `resume` 可以是代理接受的任何值
          thread.submit(undefined, { command: { resume: true } });
        }}
      >
        Resume
      </button>
    </div>
  );
}

分支

对于每条消息,你可以使用 getMessagesMetadata() 获取消息首次出现的第一个检查点。然后,你可以从首次出现检查点之前的检查点创建新的运行,以在线程中创建新的分支。

分支可以通过以下方式创建:

  1. 编辑之前的用户消息。
  2. 请求重新生成之前的助手消息。
"use client";

import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import { useState } from "react";

function BranchSwitcher({
  branch,
  branchOptions,
  onSelect,
}: {
  branch: string | undefined;
  branchOptions: string[] | undefined;
  onSelect: (branch: string) => void;
}) {
  if (!branchOptions || !branch) return null;
  const index = branchOptions.indexOf(branch);

  return (
    <div className="flex items-center gap-2">
      <button
        type="button"
        onClick={() => {
          const prevBranch = branchOptions[index - 1];
          if (!prevBranch) return;
          onSelect(prevBranch);
        }}
      >
        Prev
      </button>
      <span>
        {index + 1} / {branchOptions.length}
      </span>
      <button
        type="button"
        onClick={() => {
          const nextBranch = branchOptions[index + 1];
          if (!nextBranch) return;
          onSelect(nextBranch);
        }}
      >
        Next
      </button>
    </div>
  );
}

function EditMessage({
  message,
  onEdit,
}: {
  message: Message;
  onEdit: (message: Message) => void;
}) {
  const [editing, setEditing] = useState(false);

  if (!editing) {
    return (
      <button type="button" onClick={() => setEditing(true)}>
        Edit
      </button>
    );
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const form = e.target as HTMLFormElement;
        const content = new FormData(form).get("content") as string;

        form.reset();
        onEdit({ type: "human", content });
        setEditing(false);
      }}
    >
      <input name="content" defaultValue={message.content as string} />
      <button type="submit">Save</button>
    </form>
  );
}

export default function App() {
  const thread = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    messagesKey: "messages",
  });

  return (
    <div>
      <div>
        {thread.messages.map((message) => {
          const meta = thread.getMessagesMetadata(message);
          const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;

          return (
            <div key={message.id}>
              <div>{message.content as string}</div>

              {message.type === "human" && (
                <EditMessage
                  message={message}
                  onEdit={(message) =>
                    thread.submit(
                      { messages: [message] },
                      { checkpoint: parentCheckpoint }
                    )
                  }
                />
              )}

              {message.type === "ai" && (
                <button
                  type="button"
                  onClick={() =>
                    thread.submit(undefined, { checkpoint: parentCheckpoint })
                  }
                >
                  <span>Regenerate</span>
                </button>
              )}

              <BranchSwitcher
                branch={meta?.branch}
                branchOptions={meta?.branchOptions}
                onSelect={(branch) => thread.setBranch(branch)}
              />
            </div>
          );
        })}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();

          const form = e.target as HTMLFormElement;
          const message = new FormData(form).get("message") as string;

          form.reset();
          thread.submit({ messages: [message] });
        }}
      >
        <input type="text" name="message" />

        {thread.isLoading ? (
          <button key="stop" type="button" onClick={() => thread.stop()}>
            Stop
          </button>
        ) : (
          <button key="submit" type="submit">
            Send
          </button>
        )}
      </form>
    </div>
  );
}

对于高级用例,你可以使用 experimental_branchTree 属性获取线程的树形表示,用于为非基于消息的图渲染分支控件。

乐观更新

你可以在向代理执行网络请求之前乐观更新客户端状态,从而为用户提供即时反馈,例如在代理看到请求之前立即显示用户消息。

const stream = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  messagesKey: "messages",
});

const handleSubmit = (text: string) => {
  const newMessage = { type: "human" as const, content: text };

  stream.submit(
    { messages: [newMessage] },
    {
      optimisticValues(prev) {
        const prevMessages = prev.messages ?? [];
        const newMessages = [...prevMessages, newMessage];
        return { ...prev, messages: newMessages };
      },
    }
  );
};

TypeScript

useStream() 钩子对 TypeScript 应用非常友好,你可以为状态指定类型,以获得更好的类型安全性和 IDE 支持。

// 定义你的类型
type State = {
  messages: Message[];
  context?: Record<string, unknown>;
};

// 在钩子中使用它们
const thread = useStream<State>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  messagesKey: "messages",
});

你还可以选择为不同场景指定类型,例如:

  • ConfigurableTypeconfig.configurable 属性的类型(默认:Record<string, unknown>
  • InterruptType:中断值的类型——即 interrupt(...) 函数的内容(默认:unknown
  • CustomEventType:自定义事件的类型(默认:unknown
  • UpdateType:提交函数的类型(默认:Partial<State>
const thread = useStream<
  State,
  {
    UpdateType: {
      messages: Message[] | Message;
      context?: Record<string, unknown>;
    };
    InterruptType: string;
    CustomEventType: {
      type: "progress" | "debug";
      payload: unknown;
    };
    ConfigurableType: {
      model: string;
    };
  }
>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  messagesKey: "messages",
});

如果你使用 LangGraph.js,你还可以重用图的注解类型。但请确保仅导入注解模式的类型,以避免导入整个 LangGraph.js 运行时(即通过 import type { ... } 指令)。

import {
  Annotation,
  MessagesAnnotation,
  type StateType,
  type UpdateType,
} from "@langchain/langgraph/web";

const AgentState = Annotation.Root({
  ...MessagesAnnotation.spec,
  context: Annotation<string>(),
});

const thread = useStream<
  StateType<typeof AgentState.spec>,
  { UpdateType: UpdateType<typeof AgentState.spec> }
>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  messagesKey: "messages",
});

事件处理

useStream() 钩子提供了多个回调选项,帮助你响应不同事件:

  • onError:在发生错误时调用。
  • onFinish:在流完成时调用。
  • onUpdateEvent:在接收到更新事件时调用。
  • onCustomEvent:在接收到自定义事件时调用。了解如何流式传输自定义事件,请参见流式传输指南
  • onMetadataEvent:在接收到包含运行 ID 和线程 ID 的元数据事件时调用。

Learn More


总结

  • 集成机制:本指南展示了如何使用 useStream() React 钩子将 LangGraph 集成到 React 应用中,处理流式传输、状态管理、分支逻辑等复杂性,提供灵活的聊天体验构建方式。
  • 核心功能
    • 安装:通过 npm install @langchain/langgraph-sdk @langchain/core 安装依赖。
    • 基本用法
      • 使用 useStream() 钩子连接 LangGraph 部署(通过 apiUrlassistantId)。
      • 渲染消息列表(thread.messages)并通过 thread.submit() 发送用户输入。
      • 管理加载状态(isLoading)和停止流(thread.stop())。
    • 高级功能
      • 线程管理:通过 threadIdonThreadId 跟踪和恢复对话,建议存储在 URL 查询参数中。
      • 消息处理:自动拼接消息片段(通过 messagesKey 指定存储键),支持 streamMode: "messages-tuple"
      • 中断:使用 thread.interrupt 处理中断,渲染确认 UI 或等待用户输入。
      • 分支:通过 getMessagesMetadata()thread.submit() 创建对话分支,支持编辑用户消息或重新生成助手消息。
      • 乐观更新:通过 optimisticValues 在请求前更新客户端状态,提供即时反馈。
      • TypeScript 支持:定义状态和配置类型(如 StateInterruptType),支持类型安全和 IDE 提示。
      • 事件处理:通过回调(如 onErroronFinishonCustomEvent)响应错误、完成、自定义事件等。
  • 使用建议
    • UI 定制:利用 useStream() 的 UI 无关设计,结合自定义组件和样式(如 CopilotKit 或 assistant-ui)构建个性化界面。
    • 线程持久化:存储 threadId 以支持会话恢复,尤其在页面刷新后。
    • 中断和分支:结合中断和分支逻辑,支持人机交互和对话回溯。
    • 性能优化:使用乐观更新减少用户等待时间,通过 onError 提供清晰的错误反馈。
    • TypeScript:定义详细类型(如 StateConfigurableType),提高代码可维护性,特别是在复杂图中重用 LangGraph.js 注解。
    • 生产部署:确保 apiUrl 指向正确的 LangGraph 部署,验证网络可达性和 API 密钥(如 LANGSMITH_API_KEY 用于跟踪)。
  • 注意事项
    • 依赖 @langchain/langgraph-sdk@langchain/core,确保版本兼容。
    • 需要运行 LangGraph 服务器(默认 http://localhost:2024),并配置正确的 assistantId
    • 如果需要更复杂的 UI(如多模态输入)或高级事件处理(如自定义事件流),可扩展 useStream() 配置。

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彬彬侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值