P115分钟

什么是 Agent?

你的团队每周都要花数小时做重复、工具密集的活:分诊 issue、更新文档、review PR、改写报告。 Agent 就是用「循环里推理 + 调用真实工具」的方式干这些活的软件。如果你能写一个 REST handler, 就能写一个 Agent,接下来 5 分钟就能让它跑起来。

SDK Focusquery()for await..ofmessage streamsession_id -> resume

5 分钟跑起来

动手试试先跑,再读

装 SDK、贴代码并观察循环。然后说清这个 Agent 服务谁、改善工作流的哪一步、产生什么可逆结果;如果脚本就够用,也要明确说明。

bash
npm i @anthropic-ai/claude-agent-sdk

然后挑一种鉴权方式——SDK 支持这三种方式:

bash
# 方式 1——Claude Code 订阅(Pro/Max,按订阅付费,无 token 单价)
npm i -g @anthropic-ai/claude-code
claude /login   # 一次性浏览器登录

# 方式 2——直连 Anthropic API key
export ANTHROPIC_API_KEY=sk-ant-...

# 方式 3——Anthropic 兼容代理(以 DeepSeek 为例;Bedrock、OpenRouter
# 等其他第三方请参考各自官方文档)
export ANTHROPIC_AUTH_TOKEN=sk-...                        # 你的 DeepSeek API key
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro[1m] # 本课使用的别名
export ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro[1m]   # 后续课程使用
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash    # 后续课程使用
# DeepSeek 完整变量列表:https://api-docs.deepseek.com/guides/coding_agents
typescript
// first-agent.ts
import { query, type SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";

const assistantText = (m: SDKAssistantMessage) =>
  m.message.content
    .filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
    .map((b) => b.text)
    .join("\n");

const response = query({
  prompt: "列出当前目录下最大的 5 个文件,并告诉我哪些看起来已经过时了。",
  options: {
    model: "sonnet",
    tools: ["Bash", "Read", "Glob"],
    allowedTools: ["Read", "Glob"], // 自动批准安全的只读工具
  },
});

let sessionId: string | undefined;
for await (const msg of response) {
  switch (msg.type) {
    case "system":
      sessionId = msg.session_id;
      console.log("session:", sessionId);
      break;
    case "assistant":
      console.log("assistant:", assistantText(msg));
      break;
    case "tool_use_summary":
      console.log("act:", msg.summary);
      break;
    case "result":
      if (msg.subtype === "success") console.log("done:", msg.result);
      break;
  }
}
bash
npx tsx first-agent.ts

你会看到一段 system → assistant → tool_use_summary → assistant → result 的流。这条流就是 Agent 循环——感知、思考、行动、循环。

piPi 等效写法循环一样,事件靠 push;provider 配置走文件

如果你更想用 Pi(开源 MIT 协议的 coding agent)来写,同样的练习长这样。本课主线仍然是 Claude——这只是给你一个可迁移性的参考,不是另一条岔路。

Pi 0.79.x 要求 Node >=22.19.0。安装 CLI 时禁用 lifecycle scripts;Pi 发布包不依赖 install scripts,这也是官方文档推荐的供应链安全路径:

bash
node --version
npm install -g --ignore-scripts @earendil-works/pi-coding-agent

如果是在本课程文件里嵌入 SDK,安装 coding-agent 包和它的 core 类型即可:

bash
npm install @earendil-works/pi-coding-agent @earendil-works/pi-agent-core

Pi 从 ~/.pi/agent/auth.json 读 provider 配置。key 字段可以是 (a) 一个环境变量名(pi 运行时去 process.env 解析),(b) 一段 !shell-command 惰性读,或 (c) 字面量字符串。最快路径用内置的 DeepSeek provider:

bash
mkdir -p ~/.pi/agent

# 如果 DEEPSEEK_API_KEY 已经 export 在 shell 里,这样就够了:
cat > ~/.pi/agent/auth.json <<'JSON'
{ "deepseek": { "type": "api_key", "key": "DEEPSEEK_API_KEY" } }
JSON

# 如果没 export(或不想把明文留在 pi 配置里),让 pi 通过 shell-command 读 key 文件:
# echo "DEEPSEEK_API_KEY=sk-..." > ~/.deepseek && chmod 600 ~/.deepseek
# { "deepseek": { "type": "api_key",
#   "key": "!awk -F= '/^DEEPSEEK_API_KEY=/{print $2; exit}' ~/.deepseek" } }

chmod 600 ~/.pi/agent/auth.json

如果你要接自建或开源权重的 provider(Ant-Ling Ring、Qwen、ZenMux),把它们加到 ~/.pi/agent/models.json 即可。

typescript
// first-agent.ts
import { createAgentSession } from "@earendil-works/pi-coding-agent";
import type { AgentMessage } from "@earendil-works/pi-agent-core";

const assistantText = (m: AgentMessage): string => {
  if (m.role !== "assistant" || !Array.isArray(m.content)) return "";
  return m.content
    .filter((b): b is { type: "text"; text: string } => b.type === "text")
    .map((b) => b.text)
    .join("");
};

const { session } = await createAgentSession({ tools: ["read", "bash"] });

// Pi 在第一次发消息之前就能拿到 session id,不需要等 init 事件。
const sessionId = session.sessionId;
console.log("session:", sessionId);

const unsubscribe = session.subscribe((event) => {
  switch (event.type) {
    case "message_end":
      console.log("assistant:", assistantText(event.message));
      break;
    case "tool_execution_start":
      console.log("act:", event.toolName);
      break;
    case "agent_end": {
      const last = event.messages
        .map(assistantText)
        .filter((t) => t.length > 0)
        .pop();
      if (last) console.log("done:", last);
      break;
    }
  }
});

try {
  await session.prompt("列出当前目录下最大的 5 个文件,并告诉我哪些看起来已经过时了。");
} finally {
  unsubscribe();
}

核心差异

Provider 配置:

仅环境变量(ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)vs 配置文件(~/.pi/agent/auth.json + models.json),文件里的 key 字段又可以指向环境变量名、!shell-command 或字面量。

多 provider:

单一 Anthropic 兼容端点 / 进程 vs 在 models.json 注册多个 provider,按 session 切换。

循环形态: push(session.subscribe)vs pull(for await)。
工具暴露:

Pi 的 tools 使用小写名(["bash", "read"]);Claude Agent SDK 的 tools 使用 PascalCase(["Bash", "Read", "Glob"]),allowedTools 只是在其上叠加预批准。

看看发生了什么

每种消息类型都对应经典 感知-行动循环(Perception-Action Loop) 中的一步:

python
while not task_complete:
    observation = perceive(environment)            # tool_use_summary
    action = think(observation, goal, memory)      # assistant
    result = act(action)                           # tool_use_summary
    memory.update(observation, action, result)     # 隐式存在 session 里

query() 就是循环

循环不是你写的——query() 替你跑。你的工作是定 prompt(目标)、tools(动作空间)、 allowedTools(哪些工具无需提示就自动批准),以及决定怎么处理这些 messages(观察 + 副作用)。 这几个旋钮覆盖了 Agent 设计中最常见的取舍。

如果你来自前端或后端

这个形状应该很眼熟:query() 是 async generator,像一个 SSE handler;tools 是路由表——规定这次请求能访问哪些子 handler;allowedTools 是其中无需提示的预批准列表;session_id + resume 类似多轮会话的 session cookie。新的部分不在请求通道,而是控制流由模型决定, 不是你的代码。你的工作从「写分支」转向「设计动作空间,观察模型行为」。

继续探索 10 分钟

按顺序做这三步,每步只要一分钟,每一步展示一种独立能力。

1. 换个任务。 把 prompt 换成你团队真会问的问题:

typescript
prompt: "找出 src/ 里所有 TODO 注释,按文件分组,总结前 3 个主题。";

2. 收紧动作空间。Bashtools 里删掉,或把它放进 disallowedTools。再跑一次,看 agent 是绕过缺失的工具,还是直接放弃——工具选择是安全和作用域决策,不只是便利。

3. 续接会话。 保存第一次跑的 sessionId,然后继续对话:

typescript
const followup = query({
  prompt: "针对最热的那个主题,写一段 issue 描述。",
  options: {
    model: "sonnet",
    resume: sessionId,
    tools: ["Read", "Glob"],
    allowedTools: ["Read", "Glob"],
  },
});
piPi 等效写法直接复用 session 对象,无需 resume id
typescript
// Pi 保留有状态 session——没有 `resume: id` 参数。
// AgentSession 对象本身就是这段对话。
await session.prompt("针对最热的那个主题,写一段 issue 描述。");
续接模型:

有状态:跨轮复用 AgentSession 对象,无需传 resume: id

会话续接让不同 query() 调用延续同一段对话,但它本身不等于长期记忆或自主性。

Agent vs 聊天机器人

特性聊天机器人Agent
交互形态通常偏对话式通常偏任务导向
执行能力可生成文本或调用工具用工具检查或改变环境
自主性通常由用户驱动可在边界内选择并排序动作
状态可单轮也可多轮持久化需单独设计
失败形态答错做错 动作——后果更严重

一次性调用 vs 有状态

聊天机器人和 Agent 都可以是单轮或多轮。resume 只保留 SDK 对话状态;Agent 的关键区别是 围绕目标调用工具,并由模型在约束内选择控制流。

设计校验——什么时候不该用 Agent

Agent 贵、不确定、还能做出破坏性操作。只有以下三条全部满足,再考虑用 Agent:

  • 任务工具密集。 如果只是纯文本生成、没有外部状态,普通 prompt 又便宜又可预测。
  • 步骤事先无法穷举。 如果你能写一段 10 行脚本搞定,那就写脚本——for 循环在成本、速度、 可审计性上都赢 Agent。
  • 影响范围有边界。 Agent 做的事可以回滚(git、sandbox、staging DB、dry-run),或者写到生产前 必须有人审批。

生产差距

这一课跑的是 happy path。生产环境要处理部分失败、限流、token 预算、非确定性的重新规划。 P6(设计模式)和 P12(生产)会补上这部分——这段代码别原样上生产。

走向实战

在公司落地 这 30 行脚本就足以支撑一次小型内部试点:

  • 从一个麻烦但可回滚的工作流开始。 PR 摘要、文档时效检查、日志分诊。第一次试点不要碰会写客户数据的流程。
  • 用真实节省的工时衡量,不是「AI 采纳率」。 在 5 个真实案例上做 before/after,比任何 demo 都有说服力。
  • 把动作日志亮出来。 tool_use_summary 适合可读遥测,但不等于完整审计记录。应从工具块或 hooks 写入受保护、可关联操作者的日志,再用它解释 Agent 实际做了什么。
  • 第一个月所有写操作必须经人工审批。 之后可以放松;信任一旦崩了就很难重建。

转型为 Agent 工程师 这份工作真实的样子:

  • 你花在 动作空间(用哪些工具、各自的 scope、跑在哪个 sandbox)上的时间,会比花在 prompt 上的多。前端同学:把它当成设计一个最小化的组件 API;后端同学:当成路由表 + 鉴权中间件。
  • 面试重点会问 session 状态、工具设计、失败处理、成本控制——不是模型八卦。如果你能讲清楚 为什么选 resume 而不是重发整段 context,就已经能体现出扎实的 Agent 工程判断。
  • 有说服力的 demo 往往不大:一个真实工作流、一组可量化的 before/after、一份公开的动作日志。 一个 30 行脚本解决一个真问题,通常比复杂但缺少真实场景的多 agent demo 更有价值。

接下来

关卡:P1 完成——你能跑 query()、捕获 session_id、续接会话、向一个 Web 背景的同事 解释 Agent 循环,并且能从你工作或作品集里举出一个适合 Agent 试点的工作流。

登录后可保存课程进度、解锁闪卡,并从上次阅读的位置继续