状态机模式描述的是这样一种工作流:agent 在任务的不同状态之间移动时,其行为也会随之变化。本教程展示如何通过工具调用动态更改单个 agent 的配置来实现状态机,也就是根据当前状态更新可用工具和指令。状态可以由多个来源确定:agent 过去的动作(工具调用)、外部状态(例如 API 调用结果),甚至初始用户输入(例如运行分类器来确定用户意图)。 在本教程中,你将构建一个执行以下操作的客服 agent:
  • 继续处理前收集保修信息。
  • 将问题分类为硬件问题或软件问题。
  • 提供解决方案,或升级给人工客服。
  • 跨多个轮次维护对话状态。
不同于将 subagent 作为工具调用的 subagents 模式状态机模式使用单个 agent,并根据工作流进度更改该 agent 的配置。每个“步骤”只是同一个底层 agent 的不同配置(系统 prompt + tools),并根据状态动态选择。 下面是将要构建的工作流:

设置

安装

本教程需要 langchain 包:
npm install langchain
更多详情,请参阅安装指南

LangSmith

设置 LangSmith,以检查 agent 内部发生了什么。然后设置以下环境变量:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

选择 LLM

从 LangChain 的集成套件中选择聊天模型:
👉 Read the OpenAI chat model integration docs
npm install @langchain/openai
import { initChatModel } from "langchain";

process.env.OPENAI_API_KEY = "your-api-key";

const model = await initChatModel("gpt-5.4");

1. 定义自定义状态

首先,定义一个自定义 state schema,用于跟踪当前活跃步骤:
import { StateSchema } from "@langchain/langgraph";
import { z } from "zod";

// 定义可能的工作流步骤
const SupportStepSchema = z.enum(["warranty_collector", "issue_classifier", "resolution_specialist"]);
const WarrantyStatusSchema = z.enum(["in_warranty", "out_of_warranty"]);
const IssueTypeSchema = z.enum(["hardware", "software"]);

// 客服工作流的状态
const SupportState = new StateSchema({
  currentStep: SupportStepSchema.optional(),
  warrantyStatus: WarrantyStatusSchema.optional(),
  issueType: IssueTypeSchema.optional(),
});
current_step 字段是状态机模式的核心,它决定每一轮加载哪个配置(prompt + tools)。

2. 创建管理工作流状态的工具

创建用于更新工作流状态的工具。这些工具允许 agent 记录信息并转换到下一步。 关键是使用 Command 更新状态,包括 current_step 字段:
import { z } from "zod";
import { tool, ToolMessage, type ToolRuntime } from "langchain";
import { Command } from "@langchain/langgraph";

const recordWarrantyStatus = tool(
  async (input, config: ToolRuntime<typeof SupportState.State>) => {
    return new Command({
      update: {
        messages: [
          new ToolMessage({
            content: `保修状态已记录为:${input.status}`,
            tool_call_id: config.toolCallId,
          }),
        ],
        warrantyStatus: input.status,
        currentStep: "issue_classifier",
      },
    });
  },
  {
    name: "record_warranty_status",
    description:
      "记录客户的保修状态,并转换到问题分类步骤。",
    schema: z.object({
      status: WarrantyStatusSchema,
    }),
  }
);

const recordIssueType = tool(
  async (input, config: ToolRuntime<typeof SupportState.State>) => {
    return new Command({
      update: {
        messages: [
          new ToolMessage({
            content: `问题类型已记录为:${input.issueType}`,
            tool_call_id: config.toolCallId,
          }),
        ],
        issueType: input.issueType,
        currentStep: "resolution_specialist",
      },
    });
  },
  {
    name: "record_issue_type",
    description:
      "记录问题类型,并转换到解决方案专家步骤。",
    schema: z.object({
      issueType: IssueTypeSchema,
    }),
  }
);

const escalateToHuman = tool(
  async (input) => {
    // 在真实系统中,这会创建工单、通知员工等。
    return `正在升级给人工客服。原因:${input.reason}`;
  },
  {
    name: "escalate_to_human",
    description: "将工单升级给人工客服专家。",
    schema: z.object({
      reason: z.string(),
    }),
  }
);

const provideSolution = tool(
  async (input) => {
    return `已提供解决方案:${input.solution}`;
  },
  {
    name: "provide_solution",
    description: "为客户问题提供解决方案。",
    schema: z.object({
      solution: z.string(),
    }),
  }
);
请注意,record_warranty_statusrecord_issue_type 会返回 Command 对象,同时更新数据(warranty_statusissue_type)和 current_step。这就是状态机的工作方式:工具控制工作流进度。

3. 定义步骤配置

为每个步骤定义 prompt 和工具。首先,定义每个步骤的 prompt:
// 将 prompt 定义为常量,便于引用
const WARRANTY_COLLECTOR_PROMPT = `你是帮助客户处理设备问题的客服 agent。

当前阶段:保修验证

在此步骤中,你需要:
1. 热情地问候客户
2. 询问客户的设备是否在保修期内
3. 使用 record_warranty_status 记录客户回答并进入下一步

保持自然、友好的对话风格。不要一次询问多个问题。`;

const ISSUE_CLASSIFIER_PROMPT = `你是帮助客户处理设备问题的客服 agent。

当前阶段:问题分类
客户信息:保修状态为 {warranty_status}

在此步骤中,你需要:
1. 要求客户描述问题
2. 判断这是硬件问题(物理损坏、部件损坏)还是软件问题(app 崩溃、性能问题)
3. 使用 record_issue_type 记录分类并进入下一步

如果不清楚,请先提澄清问题,再进行分类。`;

const RESOLUTION_SPECIALIST_PROMPT = `你是帮助客户处理设备问题的客服 agent。

当前阶段:解决方案
客户信息:保修状态为 {warranty_status},问题类型为 {issue_type}

在此步骤中,你需要:
1. 对于软件问题:使用 provide_solution 提供故障排查步骤
2. 对于硬件问题:
   - 如果在保修期内:使用 provide_solution 说明保修维修流程
   - 如果超出保修期:使用 escalate_to_human 处理付费维修选项

解决方案要具体且有帮助。`;
然后使用字典将步骤名称映射到对应配置:
// 步骤配置:将步骤名称映射到(prompt、tools、required_state)
const STEP_CONFIG = {
  warranty_collector: {
    prompt: WARRANTY_COLLECTOR_PROMPT,
    tools: [recordWarrantyStatus],
    requires: [],
  },
  issue_classifier: {
    prompt: ISSUE_CLASSIFIER_PROMPT,
    tools: [recordIssueType],
    requires: ["warrantyStatus"],
  },
  resolution_specialist: {
    prompt: RESOLUTION_SPECIALIST_PROMPT,
    tools: [provideSolution, escalateToHuman],
    requires: ["warrantyStatus", "issueType"],
  },
} as const;
这种基于字典的配置让以下操作更容易:
  • 一眼查看所有步骤
  • 添加新步骤(添加另一个条目即可)
  • 理解工作流依赖关系(requires 字段)
  • 使用带状态变量的 prompt 模板(例如 {warranty_status}

4. 创建基于步骤的 middleware

创建 middleware,从 state 读取 current_step 并应用合适的配置。这里使用 @wrap_model_call decorator 来保持实现简洁:
import { createMiddleware } from "langchain";

const applyStepMiddleware = createMiddleware({
  name: "applyStep",
  stateSchema: SupportState,
  wrapModelCall: async (request, handler) => {
    // 获取当前步骤(首次交互默认使用 warranty_collector)
    const currentStep = request.state.currentStep ?? "warranty_collector";

    // 查找步骤配置
    const stepConfig = STEP_CONFIG[currentStep];

    // 验证必需状态是否存在
    for (const key of stepConfig.requires) {
      if (request.state[key] === undefined) {
        throw new Error(`到达 ${currentStep} 前必须先设置 ${key}`);
      }
    }

    // 使用状态值格式化 prompt(支持 {warrantyStatus}、{issueType} 等)
    let systemPrompt: string = stepConfig.prompt;
    for (const [key, value] of Object.entries(request.state)) {
      systemPrompt = systemPrompt.replace(`{${key}}`, String(value ?? ""));
    }

    // 注入 system prompt 和步骤专属工具
    return handler({
      ...request,
      systemPrompt,
      tools: [...stepConfig.tools],
    });
  },
});
此 middleware 会:
  1. 读取当前步骤:从 state 获取 current_step(默认值为 “warranty_collector”)。
  2. 查找配置:在 STEP_CONFIG 中找到匹配条目。
  3. 验证依赖项:确保必需的 state 字段存在。
  4. 格式化 prompt:将状态值注入 prompt 模板。
  5. 应用配置:覆盖 system prompt 和可用工具。
request.override() 方法是关键,它允许你根据状态动态更改 agent 行为,而无需创建单独的 agent 实例。

5. 创建 agent

现在,使用基于步骤的 middleware 和用于状态持久化的 checkpointer 创建 agent:
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";

// 收集所有步骤配置中的全部工具
const allTools = [
  recordWarrantyStatus,
  recordIssueType,
  provideSolution,
  escalateToHuman,
];

// 初始化模型
const model = new ChatOpenAI({
  model: "gpt-5.4-mini",
  temperature: 0.7,
});

// 使用基于步骤的配置创建 agent
const agent = createAgent({
  model,
  tools: allTools,
  stateSchema: SupportState,
  middleware: [applyStepMiddleware],
  checkpointer: new MemorySaver(),
});
为什么需要 checkpointer? checkpointer 会跨对话轮次维护状态。没有它,current_step 状态会在用户消息之间丢失,从而破坏工作流。

6. 测试工作流

测试完整工作流:
import { HumanMessage } from "@langchain/core/messages";
import { v7 as uuid7 } from "uuid";

// 此对话 thread 的配置
const threadId = uuid7();
const config = { configurable: { thread_id: threadId } };

// 第 1 轮:初始消息,从 warranty_collector 步骤开始
console.log("=== 第 1 轮:收集保修信息 ===");
let result = await agent.invoke(
  { messages: [new HumanMessage("你好,我的手机屏幕碎了")] },
  config
);
for (const msg of result.messages) {
  console.log(msg.content);
}

// 第 2 轮:用户回答保修信息
console.log("\n=== 第 2 轮:保修回答 ===");
result = await agent.invoke(
  { messages: [new HumanMessage("是的,它还在保修期内")] },
  config
);
for (const msg of result.messages) {
  console.log(msg.content);
}
console.log(`当前步骤:${result.currentStep}`);

// 第 3 轮:用户描述问题
console.log("\n=== 第 3 轮:问题描述 ===");
result = await agent.invoke(
  { messages: [new HumanMessage("屏幕摔落后出现了物理裂纹")] },
  config
);
for (const msg of result.messages) {
  console.log(msg.content);
}
console.log(`当前步骤:${result.currentStep}`);

// 第 4 轮:解决方案
console.log("\n=== 第 4 轮:解决方案 ===");
result = await agent.invoke(
  { messages: [new HumanMessage("我应该怎么办?")] },
  config
);
for (const msg of result.messages) {
  console.log(msg.content);
}
预期流程:
  1. 保修验证步骤:询问保修状态
  2. 问题分类步骤:询问问题并判断为硬件问题
  3. 解决方案步骤:提供保修维修说明

7. 理解状态转换

下面跟踪每一轮发生的事情:

第 1 轮:初始消息

{
  messages: [new HumanMessage("你好,我的手机屏幕碎了")],
  currentStep: "warranty_collector"  // 默认值
}
Middleware 会应用:
  • 系统 prompt:WARRANTY_COLLECTOR_PROMPT
  • 工具:[record_warranty_status]

第 2 轮:记录保修信息后

工具调用:recordWarrantyStatus("in_warranty") 返回:
new Command({
  update: {
    warrantyStatus: "in_warranty",
    currentStep: "issue_classifier"  // 状态转换!
  }
})
下一轮,middleware 会应用:
  • 系统 prompt:ISSUE_CLASSIFIER_PROMPT(使用 warranty_status="in_warranty" 格式化)
  • 工具:[record_issue_type]

第 3 轮:问题分类后

工具调用:recordIssueType("hardware") 返回:
new Command({
  update: {
    issueType: "hardware",
    currentStep: "resolution_specialist"  // 状态转换!
  }
})
下一轮,middleware 会应用:
  • 系统 prompt:RESOLUTION_SPECIALIST_PROMPT(使用 warranty_statusissue_type 格式化)
  • 工具:[provide_solution, escalate_to_human]
关键洞察是:工具通过更新 current_step 驱动工作流,而 middleware 会响应这一变化,在下一轮应用合适的配置。

8. 管理消息历史

随着 agent 推进各个步骤,消息历史会增长。使用 summarization middleware 压缩较早消息,同时保留对话上下文:
import { createAgent, SummarizationMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model,
  tools: allTools,
  stateSchema: SupportState,
  middleware: [
    applyStepMiddleware,
    new SummarizationMiddleware({
      model: "gpt-5.4-mini",
      trigger: { tokens: 4000 },
      keep: { messages: 10 },
    }),
  ],
  checkpointer: new MemorySaver(),
});
如需其他记忆管理技术,请参阅短期记忆指南

9. 增加灵活性:返回上一步

有些工作流需要允许用户返回先前步骤来更正信息(例如更改保修状态或问题分类)。但是,并非所有转换都有意义,例如退款处理完成后通常不能回退。在这个客服工作流中,你将添加工具,用于返回保修验证步骤和问题分类步骤。
如果你的工作流要求在大多数步骤之间任意转换,请考虑是否真的需要结构化工作流。这个模式最适合步骤按清晰顺序推进,并偶尔通过向后转换来修正信息的场景。
向解决方案步骤添加“返回”工具:
import { tool } from "langchain";
import { Command } from "@langchain/langgraph";
import { z } from "zod";

const goBackToWarranty = tool(  
  async () => {
    return new Command({ update: { currentStep: "warranty_collector" } });
  },
  {
    name: "go_back_to_warranty",
    description: "返回保修验证步骤。",
    schema: z.object({}),
  }
);

const goBackToClassification = tool(  
  async () => {
    return new Command({ update: { currentStep: "issue_classifier" } });
  },
  {
    name: "go_back_to_classification",
    description: "返回问题分类步骤。",
    schema: z.object({}),
  }
);

// 更新 resolution_specialist 配置,以包含这些工具
STEP_CONFIG.resolution_specialist.tools.push(
  goBackToWarranty,
  goBackToClassification
);
更新解决方案专家的 prompt,以提及这些工具:
const RESOLUTION_SPECIALIST_PROMPT = `你是帮助客户处理设备问题的客服 agent。

当前阶段:解决方案
客户信息:保修状态为 {warrantyStatus},问题类型为 {issueType}

在此步骤中,你需要:
1. 对于软件问题:使用 provide_solution 提供故障排查步骤
2. 对于硬件问题:
   - 如果在保修期内:使用 provide_solution 说明保修维修流程
   - 如果超出保修期:使用 escalate_to_human 处理付费维修选项

如果客户表示任何信息有误,请使用:
- go_back_to_warranty 来更正保修状态
- go_back_to_classification 来更正问题类型

解决方案要具体且有帮助。`;
现在,agent 可以处理更正:
const result = await agent.invoke(
  { messages: [new HumanMessage("实际上我搞错了,我的设备已经过保了")] },
  config
);
// Agent 会调用 go_back_to_warranty 并重新启动保修验证步骤

完整示例

下面是整合后的可运行脚本:

后续步骤