首页
/ Cline SDK Agent 模式实战:交互式 CLI、流式 UI、结构化输出与状态恢复

Cline SDK Agent 模式实战:交互式 CLI、流式 UI、结构化输出与状态恢复

2026-09-03 16:09:30作者:申梦珏Efrain

本文围绕 Cline SDK 仓库中 .agents/skills/cline-sdk/references/agent/patterns.md 收录的八类 Agent 使用模式展开:交互式 CLI Agent、按会话线程维护记忆的对话型 Agent、基于事件订阅的流式 UI、通过 Completion Tool 提取结构化数据、带超时/中断的 Agent、插件化 Agent、跨会话状态恢复,以及通过 Gateway 注入预构建模型的进阶配置。读完本文后,你可以直接复制这些可运行骨架到自己的项目中,并理解每个模式背后 @cline/agents 运行时(AgentRuntime)的实际执行逻辑。

背景:Agent 类与两种配置形态

上述所有模式都围绕同一个类:@cline/sdk 导出的 Agent。从源码看,Agent 只是 AgentRuntime 的别名,二者是同一个类,别名存在的目的是让独立使用者写更友好的构造方式(见 agent-runtime.ts 的注释与 export const Agent = AgentRuntime):

// sdk/packages/agents/src/agent-runtime.ts
/**
 * `Agent` is the user-friendly name for `AgentRuntime`. They are the same
 * class; this alias exists so standalone callers can write:
 *
 *     const agent = new Agent({ providerId, modelId, apiKey });
 *     await agent.run("hello");
 *
 * while `@cline/core` (which owns model construction) continues to use
 * the `AgentRuntime` name with `{ model, ... }` configs.
 */
export const Agent = AgentRuntime;

运行时配置是一个可判别联合类型,存在两种形态(见 agent-runtime.ts):

  • Provider 形态(推荐,独立使用者):提供 providerId + modelId + 可选 apiKey/baseUrl/headers/options,运行时内部通过 @cline/llmscreateGateway 自行构建 AgentModel
  • 预构建模型形态(进阶):直接传入 model: AgentModel,用于 @cline/core 这类需要共享 gateway/telemetry 装配的场景。

两种形态在构造函数中通过 resolveRuntimeConfig 统一解析:Provider 形态会调用 createGateway({ providerConfigs: [...] })gateway.createAgentModel({ providerId, modelId }) 生成模型,同时把 messageModelInfo 默认设为 { id: modelId, provider: providerId },使 assistant 消息自动带上模型标签(见 resolveRuntimeConfig)。完整类型定义可参考 Agent API 参考

另外两个工厂函数也可用:createAgent(config) / createAgentRuntime(config) 等价于 new(见 agents 包入口)。

模式一:交互式 CLI Agent(终端多轮对话 + 流式输出)

仓库给出的模式骨架是一个多轮终端对话 Agent:Agent 负责执行,readline 负责输入,subscribe() 负责把 assistant-text-delta 事件增量打印到终端:

import { Agent } from "@cline/sdk"
import * as readline from "node:readline"

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  apiKey: process.env.ANTHROPIC_API_KEY,
  systemPrompt: "You are a helpful assistant. Keep responses concise.",
  tools: [],
})

agent.subscribe((event) => {
  if (event.type === "assistant-text-delta") {
    process.stdout.write(event.text)
  }
})

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})

function prompt(): void {
  rl.question("\nYou: ", async (input) => {
    const trimmed = input.trim()
    if (!trimmed || trimmed === "exit") {
      rl.close()
      return
    }

    process.stdout.write("\nAssistant: ")

    if (agent.hasRun) {
      await agent.continue(trimmed)
    } else {
      await agent.run(trimmed)
    }

    process.stdout.write("\n")
    prompt()
  })
}

prompt()

几个实现层面的要点:

  • hasRun 决定调 run() 还是 continue()。API 参考将其定义为“run() 是否至少被调用过一次”的布尔属性(见 api.md 的 Methods 一节)。从源码结构看,run()continue() 内部走同一个执行入口 execute(input)agent-runtime.ts),因此文档约定“首轮用 run()、后续轮用 continue()”,并用 hasRun 来分流(gotchas.md 的 “run() vs continue()” 一节)。
  • subscribe()run()。事件监听必须注册在 run() 启动之前,否则可能丢失早期事件;subscribe() 返回一个取消函数,可在不再需要时调用以移除监听(源码中 subscribe 实现见 agent-runtime.ts)。
  • 递归 prompt() 保持循环,输入 exit 或空行时 rl.close() 退出。

模式二:对话型 Agent(Slack Bot、聊天应用)

多租户场景(如 Slack Bot)中,每个会话线程维护一个独立 Agent 实例来保存对话记忆,用 Map<string, Agent> 按线程 ID 管理:

import { Agent } from "@cline/sdk"

const agents = new Map<string, Agent>()

async function handleMessage(threadId: string, message: string) {
  let agent = agents.get(threadId)
  if (!agent) {
    agent = new Agent({
      providerId: "anthropic",
      modelId: "claude-sonnet-4-6",
      systemPrompt: "You are a concise assistant.",
      tools: [],
    })
    agents.set(threadId, agent)
  }

  const result = agent.hasRun
    ? await agent.continue(message)
    : await agent.run(message)

  return result.outputText
}

注意事项(来自 gotchas.md 的 “Memory and Long Conversations”):

  • Agent 将所有消息保存在内存中,长会话下内存随轮次增长;
  • 可通过监控 result.usage.totalInputTokens 跟踪上下文增长;
  • 超长会话建议改用带压缩(compaction)能力的 ClineCore,或定期用会话摘要新建 Agent 实例。

模式三:流式 UI(事件驱动渲染)

构建实时 UI 的核心是 subscribe() 回调里按事件类型分发。仓库示例覆盖了五类事件:assistant-text-delta(文本增量)、assistant-message(单条 assistant 消息完成)、turn-started / turn-finished(一轮迭代开始/结束,含工具调用数)、usage-updated(token 用量更新):

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "You are a helpful assistant.",
  tools: [myTool],
})

agent.subscribe((event) => {
  switch (event.type) {
    case "assistant-text-delta":
      ui.appendText(event.text)
      break
    case "assistant-message":
      ui.endText()
      break
    case "turn-started":
      ui.startTurn(event.iteration)
      break
    case "turn-finished":
      if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
      break
    case "usage-updated":
      ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
      break
  }
})

const result = await agent.run("Hello!")

从源码可以印证这些事件确实由运行时逐轮发出:主循环中每轮先 emit({ type: "turn-started", iteration }),模型返回后 emit({ type: "assistant-message", ... }),执行完工具调用后 emit({ type: "turn-finished", toolCallCount })(见 execute 主循环);此外还有 run-startedrun-finishedrun-failedmessage-addedstatus-notice 等事件。turn-finished 中的 toolCallCount 即本轮模型发起的工具调用数量(无工具调用时为 0,此时通常意味着本轮自然结束)。完整事件类型清单见 events 参考

另一个易错点:AgentRuntimeConfig 没有顶层 onEvent 字段。要接收事件只有两条路:同步的 subscribe()(适合 UI 流式渲染),或 hooks.onEvent(回调会被 await,适合写日志等异步副作用)。两者接收的事件类型相同(gotchas.md 的 “No Top-Level onEvent on Agent Config”)。

模式四:通过 Completion Tool 获取结构化输出

这是让 Agent 输出结构化数据的推荐做法:定义一个 lifecycle: { completesRun: true } 的工具,Agent 调用该工具即结束本次运行,工具入参就是你想要的结构化 schema:

import { Agent, createTool } from "@cline/sdk"
import { z } from "zod"

const submitReview = createTool({
  name: "submit_review",
  description: "Submit the final code review with structured feedback.",
  inputSchema: z.object({
    summary: z.string(),
    issues: z.array(z.object({
      file: z.string(),
      line: z.number(),
      severity: z.enum(["error", "warning", "info"]),
      message: z.string(),
    })),
    approved: z.boolean(),
  }),
  lifecycle: { completesRun: true },
  execute: async (input) => input,
})

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "Review the code diff and submit structured feedback.",
  tools: [submitReview],
})

const result = await agent.run(diffContent)
const review = result.toolCalls.find(tc => tc.name === "submit_review")
console.log(review?.output)

底层机制可以从源码得到完整印证:

  • 运行时通过 getRequiredCompletionToolNames() 收集所有 tool.lifecycle?.completesRun === true 的工具名(agent-runtime.ts);
  • 若配置了完成策略,运行开始时会自动注入一条系统提醒:“This run is not complete until you call one of these terminal completion tools: …”(getCompletionToolReminderMessage);
  • 每轮工具执行后,findCompletingToolMessage() 检查是否存在终态工具消息,命中即 finishRun("completed", ...) 并返回,结束 Agent 循环(主循环尾部);
  • 若模型某轮只返回文本、没有调用完成工具,运行时会根据 completionPolicy 注入提醒消息并继续下一轮迭代(execute 中 completion reminder 分支)。

实践要点(见 gotchas.md):

  • execute 抛出异常会被计为一次“错误”,多次后 Agent 会停止;错误应作为结构化数据返回,例如 return { error: "File not found", path: input.path }
  • 工具入参 schema 直接影响模型行为:固定取值用 z.enum(),每个属性都应有描述,约束条件(限流、上限)写进工具 description
  • 工具创建详见 tools 参考

模式五:带中断/超时的 Agent

长时间任务需要可控的中断能力。模式示例用 setTimeout 触发 agent.abort("Timeout"),然后检查 result.status

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "Analyze this data.",
  tools: [],
})

const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)

try {
  const result = await agent.run(data)
  if (result.status === "aborted") {
    console.log("Agent was aborted")
  } else {
    console.log(result.outputText)
  }
} finally {
  clearTimeout(timeout)
}

源码中的对应实现:

  • abort(reason?) 创建 AgentRuntimeAbortError(reason 为字符串时直接作为消息,Error 实例取其 message),记录到 state.lastError 后触发内部 AbortController.abort()agent-runtime.tsabort 方法);
  • 执行循环捕获到中止后,status 置为 "aborted"(而非 "failed"),最终 AgentRunResult.status 取值有三种:"completed" | "aborted" | "failed"execute 的 catch 分支);
  • 注意 run() 被 abort 时不会抛出异常,而是返回带 status: "aborted" 的 result,因此模式示例中的分支判断是必要的。

另外,长耗时工具的 execute 应尊重 context.abortSignal,在中止时返回部分结果而不是挂起(gotchas.md 的 “Abort Signal Handling in Tools”)。

模式六:带插件的 Agent

plugins 配置接受 AgentPlugin[],插件可以声明能力清单(manifest.capabilities)、在 setup() 中返回附加的工具与钩子:

import { Agent } from "@cline/sdk"
import type { AgentPlugin } from "@cline/sdk"

const loggingPlugin: AgentPlugin = {
  name: "logging",
  manifest: { capabilities: ["hooks"] },
  setup() {},
  hooks: {
    beforeTool({ toolCall }) {
      console.log(`Calling tool: ${toolCall.toolName}`)
    },
    afterRun({ result }) {
      console.log(`Completed in ${result.iterations} iterations`)
    },
  },
}

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "You are a helpful assistant.",
  tools: [myTool],
  plugins: [loggingPlugin],
})

运行时的插件装配发生在惰性初始化 initialize() 中:每个插件的 setup() 收到 { agentId, agentRole, systemPrompt } 上下文,其返回值里的 tools 会被注册进运行时工具表,hooks 会被合并注册——即插件既贡献工具也贡献生命周期钩子(agent-runtime.ts)。钩子的完整集合为 beforeRun / afterRun / beforeModel / afterModel / beforeTool / afterTool / onEvent,其中 beforeRunafterModelbeforeTool 可返回停止控制(stop control)来中止 Agent 循环(api.md 的 AgentRuntimeHooks 一节)。更多插件示例(通知、遥测、环境拦截等)见仓库的 sdk/examples/plugins 目录与 plugins 参考

模式七:跨会话状态恢复(snapshot / restore)

Agent 提供手动快照与恢复能力:snapshot() 导出当前状态(含 messages),持久化 messages,之后用 restore() 注入新实例继续对话:

// Save state
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)

// Later: restore
const agent2 = new Agent({ ...config })
const messages = JSON.parse(serialized)
agent2.restore(messages)
const result = await agent2.continue("Continue where we left off")

两个方法的源码语义值得注意:

  • snapshot() 返回一份深拷贝的 AgentRuntimeStateSnapshotagentIdrunIdstatusiterationmessages(克隆)、pendingToolCallsusagelastError 等(snapshot 实现);
  • restore(messages) 会先中止任何进行中的运行(内部调用 abort("Agent state restored")),重置 runId/status/iteration/usage/错误状态,然后把消息历史替换为传入内容;同时保留底层模型、工具、钩子、插件与已注册的 subscribe() 监听者,因此外部持久化会话后重新注入历史不会丢失订阅(restore 实现 及其注释)。

文档同时给出选择建议:若需要自动持久化(含压缩、恢复策略),改用 ClineCore,其参考见 clinecore/REFERENCE.md

模式八:通过 Gateway 注入预构建模型

Provider 形态之外,还可以显式构建模型再注入 Agent——这是 AgentRuntimeConfigWithModel 形态的典型用法,适合需要精细控制 provider 配置(多 provider 网关、自定义装配)的场景:

import { Agent } from "@cline/sdk"
import { createGateway } from "@cline/llms"

const gateway = createGateway({
  providerConfigs: [
    { providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
    { providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
  ],
})

const model = gateway.createAgentModel({
  providerId: "anthropic",
  modelId: "claude-opus-4-7",
})

const agent = new Agent({
  model,
  systemPrompt: "You are a helpful assistant.",
  tools: [],
})

对照源码可以确认:Provider 形态内部走的路径正是 createGateway({ providerConfigs: [...] }) + gateway.createAgentModel(...)resolveRuntimeConfig)。换言之,模式八是把运行时内部替你做的事显式提出来——你可以构建多 provider 的 gateway、复用同一个 AgentModel 给多个 Agent、或接入自有遥测装配,而 Provider 形态则是最省事的等价路径。

实践检查清单

综合 patterns 文档与 gotchas.md,落地这些模式时的检查项:

  1. 事件订阅时机subscribe() 必须在 run() 之前注册,且 subscribe() 回调是同步调用(hooks.onEvent 才是被 await 的);
  2. 多轮对话分流:首轮 run()、后续 continue(),用 agent.hasRun 判断(API 语义见 api.md);
  3. 终止控制:希望 Agent 显式结束时提供 completesRun: true 工具并在 system prompt 中引导模型调用它;没有工具时,模型返回不含工具调用的文本即自然完成;
  4. 错误处理:工具 execute 返回错误数据而非抛异常;result.status === "failed" 时读取 result.error
  5. 上下文增长:监控 result.usage.totalInputTokens,长会话考虑 ClineCore 的自动压缩;
  6. 认证排障apiKeyproviderId 匹配;OpenAI 兼容 provider 需同时设置 apiKeybaseUrl

相关文档

登录后查看全文
热门项目推荐
相关项目推荐