Skip to content

如何将LangGraph集成到您的React应用程序中

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

主要特性:

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

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

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

安装

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

示例

"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()}>
            停止
          </button>
        ) : (
          <button keytype="submit">发送</button>
        )}
      </form>
    </div>
  );
}

自定义您的用户界面

useStream() 钩子处理了所有复杂的后台状态管理,为您提供简单易用的接口来构建您的用户界面。以下是您开箱即得的功能:

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

这里有一些关于如何有效使用这些功能的例子:

加载状态

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()}>
          停止
        </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-key" 来接收消息流(即单个 LLM 令牌)从任何 LangChain 聊天模型调用中。更多关于消息流的信息可以在 如何从您的图中流式传输消息 指南中找到。

中断

useStream() 钩子暴露了 interrupt 属性,该属性将填充线程中的最后一个中断。您可以使用中断来:

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

更多关于中断的信息可以在 如何处理中断 指南中找到。

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 } });
        }}
      >
        继续
      </button>
    </div>
  );
}

分支

对于每个消息,您可以使用 getMessagesMetadata() 获取消息首次被看到的第一个检查点。然后,您可以从先于第一个看到的检查点的检查点创建一个新的运行,从而在线程中创建一个新的分支。

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

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

import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
  Annotation,
  MessagesAnnotation,
  type StateType,
  type UpdateType,
} from "@langchain/langgraph/web";
import { useState } from "react";

const AgentState = Annotation.Root({
  ...MessagesAnnotation.spec,
});

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);
        }}
      >
        上一个
      </button>
      <span>
        {index + 1} / {branchOptions.length}
      </span>
      <button
        type="button"
        onClick={() => {
          const nextBranch = branchOptions[index + 1];
          if (!nextBranch) return;
          onSelect(nextBranch);
        }}
      >
        下一个
      </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)}>
        编辑
      </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">保存</button>
    </form>
  );
}

export default function App() {
  const thread = useStream<
    StateType<typeof AgentState.spec>,
    { UpdateType: UpdateType<typeof AgentState.spec> }
  >({
    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>重新生成</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()}>
            停止
          </button>
        ) : (
          <button key="submit" type="submit">
            发送
          </button>
        )}
      </form>
    </div>
  );
}

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

TypeScript

useStream() 钩子对使用 TypeScript 编写的应用程序友好,您可以为状态指定类型,以获得更好的类型安全和 IDE 支持。

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

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

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

  • ConfigurableType: config.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的元数据事件时被调用。

了解更多

Comments