01. 从 Agent 到 Deep Agent:概念、Harness 与快速上手
12 min read

01. 从 Agent 到 Deep Agent:概念、Harness 与快速上手

从一次工具调用走向可持续工作的 Deep Agent:理解 Agent harness、LangGraph 与 deepagentsjs 的分层关系,并跑通第一个 TypeScript Agent。

01. 从 Agent 到 Deep Agent:概念、Harness 与快速上手#

1. 一个模型会调用工具,还不等于能完成长任务#

让模型调用天气工具很容易。真实工作往往是:理解目标、拆分问题、搜索多个来源、保存中间证据、交给不同角色分析、生成报告、等待审批、恢复运行并记录结果。

这类任务的困难不在于单次 prompt,而在于运行过程:

  1. 工具返回太多内容,消息上下文迅速膨胀;
  2. 多步骤工作没有可见的计划和阶段状态;
  3. 搜索、分析、写作需要不同的工具和上下文;
  4. 文件写入、发信、部署等动作不能直接交给模型;
  5. 任务可能超时、被中断,或需要几小时后继续。

Deep Agent 的重点就是提供一个 agent harness:模型仍然负责选择下一步,但文件、子 Agent、上下文管理、状态、审批和 backend 都有明确的运行时位置。

2. 三个层次#

层次负责什么典型问题
LangChain模型、工具、middleware、消息模型能否调用工具?schema 是否正确?
LangGraph有状态、可恢复、可流式的图执行失败后从哪里恢复?节点如何重试?
deepagentsjs面向长任务的预置 harness文件、子 Agent、计划和上下文如何协作?

createDeepAgent() 返回的不是只能问答的黑盒服务,它仍然可以 invokestreamEvents,并接入 LangGraph 的 checkpointer、store 和运行时配置。

3. 默认能力和可选能力#

typescript
import { createDeepAgent } from "deepagents";

const agent = createDeepAgent({
  model: "provider:model-name",
});

const result = await agent.invoke({
  messages: [
    { role: "user", content: "把这项工作拆成可执行的步骤,并说明中间结果应保存在哪里。" },
  ],
});

console.log(result.messages.at(-1)?.content);

默认文件工具通常包括:

text
ls          查看目录和文件元数据
read_file   读取文件,可按 offset/limit 分段
write_file  创建或覆盖文件
edit_file   对已有文件做精确字符串替换
glob        按路径模式查找文件
grep        搜索文件内容

普通 StateBackend 没有 shell 执行能力,因此不能从“有文件工具”推断出 Agent 可以运行任意命令。任务规划也不是无条件存在的默认工具,后文会显式加入 todoListMiddleware()

4. Agent harness 的数据流#

text
用户输入

模型读取系统规则、工具描述和当前状态

直接回答,或选择工具 / task / 文件操作

工具结果进入上下文,长结果可卸载到 backend

模型继续决策,直到完成、失败或 interrupt

最终消息 + state + 可选 checkpoint / trace

工具描述“能做什么”,backend 决定“写到哪里”,middleware 决定“如何组织行为”,checkpointer 决定“能否恢复”。把这些职责混在 system prompt 里,通常会造成不可测试和不可审计的运行时。

5. 初始化 TypeScript 项目#

bash
mkdir deep-agent-demo && cd deep-agent-demo
npm init -y
npm install deepagents langchain @langchain/core @langchain/openai zod
npm install -D typescript tsx @types/node
npx tsc --init --module NodeNext --moduleResolution NodeNext --target ES2022
mkdir src

package.json 中加入:

json
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "typecheck": "tsc --noEmit"
  }
}

不要把 API key 写进源码。以 OpenAI adapter 为例,可以在 shell 中配置:

bash
export OPENAI_API_KEY="..."
export OPENAI_MODEL="gpt-4.1-mini"

模型名只是示例。你也可以使用 Google、Anthropic 或其他兼容 LangChain 的 Chat Model;重要的是它支持当前 adapter 的 tool calling。

6. 第一个完整工具 Agent#

typescript
// src/index.ts
import { ChatOpenAI } from "@langchain/openai";
import { createDeepAgent } from "deepagents";
import { tool } from "langchain";
import { z } from "zod";

const getWeather = tool(
  async ({ city }: { city: string }) => ({
    city,
    temperatureC: 22,
    condition: "sunny",
    source: "demo",
  }),
  {
    name: "get_weather",
    description: "获取演示天气。真实应用必须替换为可信 API,并在回答中说明来源。",
    schema: z.object({
      city: z.string().min(1).describe("城市名称,例如 Shanghai"),
    }),
  },
);

const model = new ChatOpenAI({
  model: process.env.OPENAI_MODEL ?? "gpt-4.1-mini",
  temperature: 0,
});

const agent = createDeepAgent({
  model,
  tools: [getWeather],
  systemPrompt: `你是可靠的天气助手。
需要天气数据时调用 get_weather。
演示工具的结果不是实时观测,不要把它描述成真实天气。`,
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "上海现在是什么天气?" }],
});

console.log(result.messages.at(-1)?.content);

运行:

bash
npm run typecheck
npm run dev

7. 一次调用发生了什么#

用户问“上海现在是什么天气”时,模型一般会先产生一个工具调用:

text
assistant: tool_call(get_weather, { city: "上海" })
tool: { city: "上海", temperatureC: 22, ... }
assistant: 根据工具结果生成回答

应用调试时不要只打印最后一句话,还要观察:工具名是否是预期的 get_weather;参数是否通过 Zod schema;工具返回错误时是否停止猜测;不需要天气的问题是否无谓调用工具;日志和 trace 是否泄露环境变量。

temperature: 0 可以减少随机差异,但不能保证模型一定调用工具。工具是否被调用仍取决于模型能力、描述和用户问题。

8. 计算器:观察多次工具调用#

typescript
const calculate = tool(
  ({ operation, left, right }: {
    operation: "add" | "subtract" | "multiply" | "divide";
    left: number;
    right: number;
  }) => {
    if (operation === "divide" && right === 0) {
      return JSON.stringify({ ok: false, code: "DIVIDE_BY_ZERO" });
    }

    const value = {
      add: left + right,
      subtract: left - right,
      multiply: left * right,
      divide: left / right,
    }[operation];

    return JSON.stringify({ ok: true, value });
  },
  {
    name: "calculate",
    description: "进行加减乘除。需要计算时必须调用此工具,不要心算。",
    schema: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]),
      left: z.number(),
      right: z.number(),
    }),
  },
);

如果用户要求“计算 (18 + 6) / 3”,模型可能先调用加法,再调用除法。它也可能选择其他合法调用方式,因为工具调用是模型决策,而不是固定 DAG。若顺序是业务硬约束,应交给确定性代码或 LangGraph。

9. 研究助手:从本地 stub 到真实搜索#

先用统一的内部接口隔离搜索供应商:

typescript
type SearchHit = {
  title: string;
  url: string;
  snippet: string;
};

async function searchDocs(query: string): Promise<SearchHit[]> {
  // 这里替换成 Tavily、供应商原生搜索或内部检索服务。
  return [{
    title: `关于 ${query} 的演示结果`,
    url: "https://example.com",
    snippet: "仅用于演示,请勿当作真实来源。",
  }];
}

const search = tool(
  async ({ query, maxResults }: { query: string; maxResults: number }) => {
    try {
      const hits = await searchDocs(query);
      return JSON.stringify({ ok: true, query, results: hits.slice(0, maxResults) });
    } catch (error) {
      console.error("search failed", { query, error });
      return JSON.stringify({
        ok: false,
        code: "SEARCH_UNAVAILABLE",
        retryable: true,
        message: "搜索服务暂时不可用,请稍后重试。",
      });
    }
  },
  {
    name: "search_docs",
    description: "搜索公开文档,返回标题、URL和摘要。必须引用返回的URL,不要虚构来源。",
    schema: z.object({
      query: z.string().min(3).max(200),
      maxResults: z.number().int().min(1).max(8).default(5),
    }),
  },
);

研究 Agent 可以这样配置:

typescript
const researchAgent = createDeepAgent({
  model,
  tools: [search],
  systemPrompt: `你是研究助手。
先把问题拆成可检索的子问题,再调用 search_docs。
只把工具返回的 URL 当作来源;没有来源时标记为未核验。
回答分成:结论、证据、限制。`,
});

真实接入 Tavily 或原生搜索时,不要把供应商的原始响应直接暴露给模型。适配层应该统一超时、重试、字段清洗、结果数量、URL 去重和日志脱敏。

10. 什么时候不该用 Deep Agent#

如果任务是一次确定性的函数调用,直接调用函数或模型更简单。Deep Agent 的初始化、状态、工具循环和观测都有成本,它真正有价值的场景是:

  • 任务需要多步推理和工具协作;
  • 中间结果很大,需要文件承接;
  • 子任务需要隔离上下文;
  • 任务可能被中断或跨请求恢复;
  • 真实副作用需要策略和审批。

11. 启动排错#

现象排查顺序
Cannot find module检查 ESM、type、NodeNext、依赖和 import 后缀
API key 缺失检查 provider 对应的环境变量和启动进程继承情况
模型不调用工具检查 tool calling、description、schema 和模型收到的工具清单
参数经常错误收紧 Zod schema,增加 enum、范围和 describe
输出忽然是对象不要假设 content 永远是 string,先检查消息类型再序列化
运行成本很高检查重复工具调用、无必要计划、过长结果和失败重试

12. 本章练习与验收#

  1. 把天气工具替换成真实或本地 mock 搜索工具。
  2. 为搜索工具增加空结果、超时和服务端错误三种返回。
  3. 打印一次完整消息链,区分模型消息、工具调用和工具结果。
  4. 让模型回答一个没有来源的问题,确认它会明确标记未核验。

验收标准:项目可以通过 tsc --noEmit;工具参数有运行时校验;工具失败不会被包装成事实;模型、工具和 provider 可以独立替换。

相关文章