Skip to main content
并非所有的智能体交互都是聊天。有时智能体正在执行一个多步计划,而展示进度的最佳方式是待办事项列表,它会实时更新。deep agent todo list 模式直接从智能体的状态中读取 todos 数组,随着智能体执行计划,渲染每个项目及其当前状态。这是一个构建在你用于聊天的同一个 useStream hook 上的进度仪表板。它表明智能体状态可以为任何 UI 提供动力,而不仅仅是消息气泡。

工作原理

在 LangGraph 智能体中,状态不仅限于消息。你可以定义自定义状态键来保存任意数据。在这种情况下,是一个 todos 数组。当智能体执行计划时,它将每个待办事项的状态从 "pending" 更新为 "in_progress" 再到 "completed"useStream hook 通过 stream.values 暴露这些自定义状态值,你的 UI 会响应式地渲染它们。 流程如下所示:
  1. 用户提交请求
  2. 智能体创建计划并在其状态中填充 todos
  3. 智能体开始执行每个待办事项,状态 transitions 通过 pendingin_progresscompleted
  4. 随着智能体进度,stream.values.todos 实时更新
  5. 你的 UI 用当前状态重新渲染待办事项列表

设置 useStream

不需要特殊配置。将 useStream 指向你的智能体并从 stream.values 读取 todos 定义一个匹配智能体状态模式的 TypeScript 接口,并将其作为类型参数传递给 useStream,以类型安全地访问状态值,包括自定义状态键(如 todos)。在下面的示例中,将 typeof myAgent 替换为你的接口名称:
import type { BaseMessage } from "@langchain/core/messages";

interface TodoItem {
  title: string;
  status: "pending" | "in_progress" | "completed";
  description?: string;
}

interface AgentState {
  messages: BaseMessage[];
  todos: TodoItem[];
}
import { useStream } from "@langchain/react";

const AGENT_URL = "http://localhost:2024";

export function TodoAgent() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  const todos = stream.values?.todos ?? [];

  return (
    <div>
      <TodoList todos={todos} />
      {stream.messages.map((msg) => (
        <Message key={msg.id} message={msg} />
      ))}
    </div>
  );
}

Todo 接口

数组中的每个待办事项都有一个简单的结构:
interface Todo {
  status: "pending" | "in_progress" | "completed";
  content: string;
}
属性描述
status此任务的当前状态。选项:pending(未开始)、in_progress(智能体正在处理)、completed(已完成)
content任务涉及内容的可读描述
智能体在创建计划时填充此数组,然后在执行每个步骤时更新单个项目。

构建 TodoList 组件

待办事项列表渲染每个项目,带有状态图标、颜色编码和反映当前状态的视觉样式:
function TodoList({ todos }: { todos: Todo[] }) {
  const completed = todos.filter((t) => t.status === "completed").length;
  const percentage = todos.length
    ? Math.round((completed / todos.length) * 100)
    : 0;

  return (
    <div className="rounded-lg border bg-white p-4 shadow-sm">
      <div className="mb-4 flex items-center justify-between">
        <h2 className="text-lg font-semibold">Agent Progress</h2>
        <span className="text-sm text-gray-500">
          {completed}/{todos.length} tasks
        </span>
      </div>

      <ProgressBar percentage={percentage} />

      <ul className="mt-4 space-y-2">
        {todos.map((todo, i) => (
          <TodoItem key={i} todo={todo} />
        ))}
      </ul>
    </div>
  );
}

进度条

视觉进度条让用户一目了然地了解整体完成情况:
function ProgressBar({ percentage }: { percentage: number }) {
  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between text-xs text-gray-500">
        <span>Progress</span>
        <span>{percentage}%</span>
      </div>
      <div className="h-2 overflow-hidden rounded-full bg-gray-200">
        <div
          className="h-full rounded-full bg-green-500 transition-all duration-500"
          style={{ width: `${percentage}%` }}
        />
      </div>
    </div>
  );
}

单个待办事项

每个项目都有一个状态图标、颜色编码的文本和已完成任务的删除线样式:
function TodoItem({ todo }: { todo: Todo }) {
  const config = {
    pending: {
      icon: "○",
      textClass: "text-gray-600",
      bgClass: "bg-gray-50",
      iconClass: "text-gray-400",
    },
    in_progress: {
      icon: "◉",
      textClass: "text-amber-800",
      bgClass: "bg-amber-50 border-amber-200",
      iconClass: "text-amber-500 animate-pulse",
    },
    completed: {
      icon: "✓",
      textClass: "text-green-800 line-through",
      bgClass: "bg-green-50 border-green-200",
      iconClass: "text-green-500",
    },
  };

  const style = config[todo.status];

  return (
    <li
      className={`flex items-start gap-3 rounded-md border px-3 py-2 ${style.bgClass}`}
    >
      <span className={`mt-0.5 text-lg leading-none ${style.iconClass}`}>
        {style.icon}
      </span>
      <span className={`text-sm ${style.textClass}`}>{todo.content}</span>
    </li>
  );
}
in_progress 图标使用 animate-pulse 来吸引人们对当前活动任务的注意。

计算进度

直接从 todos 数组派生进度指标:
const todos = stream.values?.todos ?? [];

const completed = todos.filter((t) => t.status === "completed").length;
const inProgress = todos.filter((t) => t.status === "in_progress").length;
const pending = todos.filter((t) => t.status === "pending").length;
const percentage = todos.length
  ? Math.round((completed / todos.length) * 100)
  : 0;
这些值随着智能体修改其状态而响应式更新,使进度条和计数器保持同步。

与聊天消息结合

待办事项列表与常规聊天界面协同工作。一个实用的布局将待办事项列表显示为持久化的侧边栏或头部面板,聊天消息显示在下方:
function TodoAgentLayout() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  const todos = stream.values?.todos ?? [];

  return (
    <div className="flex h-screen flex-col">
      {todos.length > 0 && (
        <div className="border-b bg-gray-50 p-4">
          <TodoList todos={todos} />
        </div>
      )}

      <main className="flex-1 overflow-y-auto p-6">
        <div className="mx-auto max-w-2xl space-y-4">
          {stream.messages.map((msg) => (
            <Message key={msg.id} message={msg} />
          ))}
        </div>
      </main>

      <ChatInput
        onSubmit={(text) =>
          stream.submit({ messages: [{ type: "human", content: text }] })
        }
        isLoading={stream.isLoading}
      />
    </div>
  );
}
仅在 todos.length > 0 时显示待办事项列表。在智能体创建计划之前,没有什么可显示的。显示空组件会浪费空间。

超越 todos 的自定义状态

此模式展示了一个强大的原则:stream.values 可以暴露你的智能体定义的任何自定义状态,而不仅仅是消息。todos 数组只是一个例子。你可以对以下内容使用相同的方法:
  • 进度指标stream.values.progress 带有数字完成数据
  • 生成的工件stream.values.document 带有智能体正在构建的结构化文档
  • 决策日志stream.values.decisions 跟踪智能体做出的每个选择
  • 资源列表stream.values.sources 带有智能体找到的链接和参考
// 你的智能体定义的任何自定义状态键都可访问
const document = stream.values?.document;
const sources = stream.values?.sources ?? [];
const confidence = stream.values?.confidence_score;
自定义状态键在你的 LangGraph 图的状态模式中定义。useStream hook 会自动将它们包含在 stream.values 中,无需任何额外的客户端配置。

动画化转换

待办事项状态转换实时发生,平滑的动画使这些变化感觉 polished 而不是突兀:
function TodoItem({ todo }: { todo: Todo }) {
  return (
    <li
      className={`
        flex items-start gap-3 rounded-md border px-3 py-2
        transition-all duration-300 ease-in-out
        ${getStatusStyles(todo.status)}
      `}
    >
      <span
        className={`
          mt-0.5 text-lg leading-none transition-colors duration-300
          ${getIconStyles(todo.status)}
        `}
      >
        {getStatusIcon(todo.status)}
      </span>
      <span
        className={`
          text-sm transition-all duration-300
          ${todo.status === "completed" ? "line-through opacity-60" : ""}
        `}
      >
        {todo.content}
      </span>
    </li>
  );
}
transition-all duration-300 类确保颜色变化、删除线和透明度 shift 都平滑动画。

用例

待办事项列表模式适合任何智能体执行结构化计划的场景:
  • 项目规划:智能体将项目分解为任务并按顺序处理
  • 研究工作流:每个研究问题成为一个待办事项,智能体调查并完成它
  • 数据处理:摄入、验证、转换和导出等步骤各自获得自己的待办事项
  • 入职流程:智能体 walkthrough 设置步骤,配置服务时检查每个步骤
  • 报告生成:报告的章节成为待办事项:收集数据、分析趋势、撰写摘要、格式化输出

处理空状态和加载状态

处理智能体创建计划之前的初始状态:
function TodoList({ todos, isLoading }: { todos: Todo[]; isLoading: boolean }) {
  if (todos.length === 0 && !isLoading) {
    return null;
  }

  if (todos.length === 0 && isLoading) {
    return (
      <div className="rounded-lg border bg-white p-4 shadow-sm">
        <div className="flex items-center gap-2 text-sm text-gray-500">
          <span className="animate-spin"></span>
          Agent is creating a plan...
        </div>
      </div>
    );
  }

  return (
    <div className="rounded-lg border bg-white p-4 shadow-sm">
      {/* ... 完整待办事项列表渲染 */}
    </div>
  );
}

最佳实践

  • 醒目地显示待办事项列表。它是基于计划的智能体的主要进度指示器。不要把它埋在折叠线以下。
  • 动画化状态转换。平滑的转换使智能体感觉响应更快。对背景颜色、文本装饰和透明度使用 CSS 转换。
  • 仅高亮显示一个 in_progress 项目。智能体通常一次处理一个任务。如果多个项目显示为 in_progress,UI 会变得嘈杂。考虑仅 pulsing 第一个。
  • 折叠或调暗已完成的项目。随着列表增长,已完成的项目变得不那么相关。减少它们的视觉权重,以便用户关注仍在发生的事情。
  • 显示进度百分比。一个像 “67% complete” 这样的单一数字即使从房间对面也能立即理解。
  • 保持待办事项列表同步。因为 stream.values 响应式更新,待办事项列表会自动保持最新。不要添加手动轮询或刷新逻辑。