01. 从 Agent 到 Deep Agent:概念、Harness 与快速上手#
1. 一个模型会调用工具,还不等于能完成长任务#
让模型调用天气工具很容易。真实工作往往是:理解目标、拆分问题、搜索多个来源、保存中间证据、交给不同角色分析、生成报告、等待审批、恢复运行并记录结果。
这类任务的困难不在于单次 prompt,而在于运行过程:
- 工具返回太多内容,消息上下文迅速膨胀;
- 多步骤工作没有可见的计划和阶段状态;
- 搜索、分析、写作需要不同的工具和上下文;
- 文件写入、发信、部署等动作不能直接交给模型;
- 任务可能超时、被中断,或需要几小时后继续。
Deep Agent 的重点就是提供一个 agent harness:模型仍然负责选择下一步,但文件、子 Agent、上下文管理、状态、审批和 backend 都有明确的运行时位置。
2. 三个层次#
| 层次 | 负责什么 | 典型问题 |
|---|---|---|
| LangChain | 模型、工具、middleware、消息 | 模型能否调用工具?schema 是否正确? |
| LangGraph | 有状态、可恢复、可流式的图执行 | 失败后从哪里恢复?节点如何重试? |
| deepagentsjs | 面向长任务的预置 harness | 文件、子 Agent、计划和上下文如何协作? |
createDeepAgent() 返回的不是只能问答的黑盒服务,它仍然可以 invoke、streamEvents,并接入 LangGraph 的 checkpointer、store 和运行时配置。
3. 默认能力和可选能力#
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);默认文件工具通常包括:
ls 查看目录和文件元数据
read_file 读取文件,可按 offset/limit 分段
write_file 创建或覆盖文件
edit_file 对已有文件做精确字符串替换
glob 按路径模式查找文件
grep 搜索文件内容普通 StateBackend 没有 shell 执行能力,因此不能从“有文件工具”推断出 Agent 可以运行任意命令。任务规划也不是无条件存在的默认工具,后文会显式加入 todoListMiddleware()。
4. Agent harness 的数据流#
用户输入
↓
模型读取系统规则、工具描述和当前状态
↓
直接回答,或选择工具 / task / 文件操作
↓
工具结果进入上下文,长结果可卸载到 backend
↓
模型继续决策,直到完成、失败或 interrupt
↓
最终消息 + state + 可选 checkpoint / trace工具描述“能做什么”,backend 决定“写到哪里”,middleware 决定“如何组织行为”,checkpointer 决定“能否恢复”。把这些职责混在 system prompt 里,通常会造成不可测试和不可审计的运行时。
5. 初始化 TypeScript 项目#
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 中加入:
{
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
}
}不要把 API key 写进源码。以 OpenAI adapter 为例,可以在 shell 中配置:
export OPENAI_API_KEY="..."
export OPENAI_MODEL="gpt-4.1-mini"模型名只是示例。你也可以使用 Google、Anthropic 或其他兼容 LangChain 的 Chat Model;重要的是它支持当前 adapter 的 tool calling。
6. 第一个完整工具 Agent#
// 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);运行:
npm run typecheck
npm run dev7. 一次调用发生了什么#
用户问“上海现在是什么天气”时,模型一般会先产生一个工具调用:
assistant: tool_call(get_weather, { city: "上海" })
tool: { city: "上海", temperatureC: 22, ... }
assistant: 根据工具结果生成回答应用调试时不要只打印最后一句话,还要观察:工具名是否是预期的 get_weather;参数是否通过 Zod schema;工具返回错误时是否停止猜测;不需要天气的问题是否无谓调用工具;日志和 trace 是否泄露环境变量。
temperature: 0 可以减少随机差异,但不能保证模型一定调用工具。工具是否被调用仍取决于模型能力、描述和用户问题。
8. 计算器:观察多次工具调用#
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 到真实搜索#
先用统一的内部接口隔离搜索供应商:
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 可以这样配置:
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. 本章练习与验收#
- 把天气工具替换成真实或本地 mock 搜索工具。
- 为搜索工具增加空结果、超时和服务端错误三种返回。
- 打印一次完整消息链,区分模型消息、工具调用和工具结果。
- 让模型回答一个没有来源的问题,确认它会明确标记未核验。
验收标准:项目可以通过 tsc --noEmit;工具参数有运行时校验;工具失败不会被包装成事实;模型、工具和 provider 可以独立替换。