AI 电子伴侣
创建时间: 2026-04-18最后更新: 2026-04-21

1. 从「一次调用」到「多步 Agent」

上一篇写了一个典型 tool calling 的例子:LLM 说「我要调 searchMemory」,我们执行,把结果回给它,它再生成最终回复。

但如果这次「生成最终回复」时 LLM 又觉得信息不够、还想再调一个工具呢?这就是 Agent 循环:

agent-loop.txt
01
user: 帮我记录今天的情绪并推荐一首适合听的歌
02
03
LLM step1: 先调 classifyEmotion
04
05
tool-result: { emotion: 'sad', intensity: 0.7 }
06
07
LLM step2: 好,现在调 searchMusic,按情绪找
08
09
tool-result: [{ title: 'x', url: 'y' }, ...]
10
11
LLM step3: 最后调 logEmotion 记录下来
12
13
tool-result: { ok: true }
14
15
LLM step4: 生成最终回复 「我记录了你今天的情绪,推荐这首...」

AI SDK 把这套循环内置了,你不用自己写 while 循环。只要给 streamText 传一个 stopWhen,它就会自动做这几件事:

  1. 调 LLM
  2. LLM 返回 tool-call → AI SDK 执行工具 → 把 tool-result 塞进消息
  3. 再调 LLM
  4. 重复,直到 LLM 不再调工具,或者触发 stopWhen

这一篇把这套机制讲透。

2. 最简 Agent 循环

看最简单的用法:

agent-simple.ts
01
import { streamText, stepCountIs } from 'ai'
02
03
const result = streamText({
04
model: models.chat,
05
messages: [
06
{ role: 'user', content: '帮我记录今天的情绪并推荐一首适合听的歌' },
07
],
08
tools: {
09
classifyEmotion,
10
searchMusic,
11
logEmotion,
12
},
13
stopWhen: stepCountIs(5), // 最多 5 step
14
})
15
16
for await (const part of result.fullStream) {
17
if (part.type === 'text-delta') process.stdout.write(part.delta)
18
if (part.type === 'tool-call') console.log('\n[调用]', part.toolName)
19
if (part.type === 'tool-result') console.log('\n[结果]', part.output)
20
if (part.type === 'finish-step') console.log('\n[step 完成]')
21
if (part.type === 'finish') console.log('\n[全部完成]', part.finishReason)
22
}

对应的 fullStream 会输出多轮 start-step / tool-call / tool-result / finish-step,最后才是 text-deltafinish

3. stopWhen:停止条件

stopWhen 控制循环什么时候结束。它可以是单个条件,也可以是条件数组(任意一个满足就停)。

3.1 stepCountIs:按步数

最常用的一个:

step-count.ts
1
import { stepCountIs } from 'ai'
2
3
stopWhen: stepCountIs(5) // 最多 5 步
4
stopWhen: stepCountIs(3) // 最多 3 步(适合简单任务)
5
stopWhen: stepCountIs(10) // 最多 10 步(复杂任务)

给多少步合适,看任务复杂度:

  • 简单任务(1-2 个工具顺序调用):3 步够
  • 中等任务(需要探索、回退):5-7 步
  • 复杂任务(多工具交叉、规划):10 步起

上限的核心意义是防死循环。LLM 有时候会陷入「调工具 → 结果不满意 → 再调 → 还不满意」的死循环,stepCountIs 是硬保险。

3.2 其他停止条件

某个工具被调用过就停,用 hasToolCall

has-tool-call.ts
1
import { hasToolCall } from 'ai'
2
3
// 只要调过 finalizeResponse 就停
4
stopWhen: hasToolCall('finalizeResponse')

适合的场景:工具箱里有一个「终结者」工具(比如 finalizeResponsecommitAction),调到它就意味着任务完成。

多个条件任一满足就停,用数组组合:

combine.ts
1
stopWhen: [
2
stepCountIs(10),
3
hasToolCall('finalizeResponse'),
4
]

stopWhen 也可以传一个函数,接收 { steps },返回 boolean:

custom-stop.ts
1
stopWhen: ({ steps }) => {
2
// 如果有任何 step 用到了超过 3000 个 input token,停
3
const totalTokens = steps.reduce((sum, s) => sum + s.usage.inputTokens, 0)
4
return totalTokens > 3000
5
}

这就给成本控制、业务规则留了很多空间。

4. 完整流:一次 Agent 循环发生了什么

用一个详细例子看清 Agent 循环内部到底发生了什么。

输入:

input.txt
1
用户: 帮我查一下上周五聊了什么,用那个话题写一段安慰的回复

工具箱:searchMemoryanalyzeEmotionfinalizeResponse

Step 1:LLM 分析,决定先检索记忆

step1.txt
1
start-step
2
text-delta: "让我先回忆一下..."
3
tool-call: searchMemory({"query": "上周五 话题"})

AI SDK 执行 searchMemory

step1-tool.txt
1
tool-result: [{content: "上周五你说工作压力大"}, ...]
2
finish-step

Step 2:LLM 拿到记忆,决定分析情绪

step2.txt
1
start-step
2
tool-call: analyzeEmotion({"text": "上周五你说工作压力大"})

AI SDK 执行 analyzeEmotion

step2-tool.txt
1
tool-result: {emotion: "stressed", intensity: 0.8}
2
finish-step

Step 3:LLM 综合信息,生成最终回复

step3.txt
1
start-step
2
text-delta: "我记得你上周五说..."
3
text-delta: "到工作压力特别大..."
4
text-delta: "希望这周好一点..."
5
finish-step
6
finish (reason: stop)

整个过程通过一个 streamText 调用、一次 fullStream 消费就完成了。

这里一个重要的细节:消息历史是自动累积的。AI SDK 内部会把每个 step 的 tool-call / tool-result 拼进下一次 LLM 调用的 messages 里,不需要你手动组装。

5. 访问 steps:拿到每一步的详情

流结束之后,可以拿到所有 step 的结构化数据:

access-steps.ts
01
const result = streamText({ model, messages, tools, stopWhen: stepCountIs(5) })
02
03
// 等流跑完
04
for await (const _ of result.textStream) { /* 消费或丢弃 */ }
05
06
// 拿到所有 step
07
const steps = await result.steps
08
for (const [i, step] of steps.entries()) {
09
console.log(`Step ${i + 1}:`)
10
console.log(' - text:', step.text)
11
console.log(' - toolCalls:', step.toolCalls)
12
console.log(' - toolResults:', step.toolResults)
13
console.log(' - usage:', step.usage)
14
console.log(' - finishReason:', step.finishReason)
15
}
16
17
// 汇总信息
18
const totalUsage = await result.usage
19
const finalText = await result.text

每个 step 就是一次完整的「LLM 调用 + 工具执行」轮次。这个结构非常适合做三件事:

  • 埋点——知道每一步多少 token、用了什么工具
  • 回放——存下来后可以重现一次 Agent 运行
  • Debug——看清楚 LLM 每一步的决策

6. prepareStep:step 之间的钩子

prepareStep 是一个非常强大的钩子。它在每次 step 开始前被调用,可以动态修改下一个 step 的行为:

prepare-step.ts
01
streamText({
02
model: models.chat,
03
messages,
04
tools: companionTools,
05
stopWhen: stepCountIs(5),
06
07
prepareStep: async ({ stepNumber, steps, messages }) => {
08
// 第一步:强制用结构化工具分类情绪
09
if (stepNumber === 0) {
10
return {
11
toolChoice: { type: 'tool', toolName: 'classifyEmotion' },
12
}
13
}
14
15
// 最后一步:禁用所有工具,强制生成文本
16
if (stepNumber === 4) {
17
return { toolChoice: 'none' }
18
}
19
20
// 其他 step 保持默认
21
return undefined
22
},
23
})

还可以在 prepareStep 里动态修改 messages、system、tools:

dynamic-step.ts
01
prepareStep: async ({ stepNumber, messages }) => {
02
// 在第 3 步的 prompt 里注入新信息
03
if (stepNumber === 2) {
04
return {
05
messages: [
06
...messages,
07
{ role: 'system', content: '请基于前两步的结果生成最终回复。' },
08
],
09
}
10
}
11
return undefined
12
}

这让你能在 AI SDK 的 Agent 循环里插入自定义逻辑,不用自己重写循环。

7. 和 LangGraph ReAct Agent 的对比

我们在 LangChain 讲过 createReactAgent。两者对比一下:

维度LangChain createReactAgentAI SDK streamText + stopWhen
循环控制Agent 内部stopWhen 参数
step 粒度观察.stream({streamMode: 'values'})fullStream 的事件
中间逻辑钩子checkpointer 机制 + 自定义节点prepareStep / onStepFinish
多 Agent 协作LangGraph supervisor / swarm不原生支持,需自己组合
HITLLangGraph interrupttool 不传 execute + 自行续传
时间旅行 / 回滚LangGraph checkpointer
状态持久化Checkpointer(PostgresSaver 等)业务层自己做
学习成本较高(要懂 Graph 概念)较低(就是一个参数)

什么时候 AI SDK 的 Agent 循环就够用:

  • 线性任务(几个工具顺序或并行调用)
  • 不需要分支、回滚、HITL
  • 不需要跨会话记忆 step 状态

什么时候需要上 LangGraph:

  • 多节点图(情绪节点 → 记忆节点 → Prompt 节点 → LLM 节点)
  • 条件路由
  • HITL / 时间旅行
  • 多 Agent 协作

对 AI 伴侣项目来说,主管线用 LangGraph(情绪状态机、混合记忆、HITL 审批),末端 LLM 节点内部则用 streamText + stopWhen 做 tool 调用。与 LangChain 协同 会展开。

8. 常见问题与小结

为什么 LLM 一直调工具不停

stopWhen 给的步数太少时容易出现这个现象。排查方向:工具 description 写得不清楚,导致 LLM 不知道什么时候算完成;schema 太松,LLM 反复尝试不同参数;缺少一个「终结者」工具(如 finalizeResponse);或者模型推理能力不够。

为什么 LLM 一次也不调工具

常见原因有几个:description 太模糊;prompt 里没引导 LLM 思考工具;模型本身 tool calling 能力弱(部分小型模型会这样)。

工具并行调用失控

如果 LLM 一次 step 里同时调 5 个工具,但你期望顺序执行,可以在 system prompt 里明确要求「一次只调一个工具」,或者用 prepareStep 控制每一步开放的工具集。

如何强制 Agent 以特定工具收尾

hasToolCall 配合 prepareStep

force-final.ts
1
// stopWhen 用 hasToolCall
2
stopWhen: hasToolCall('finalizeResponse'),
3
4
// prepareStep 在接近上限时强制调用
5
prepareStep: ({ stepNumber, steps }) => {
6
if (stepNumber >= 4) {
7
return { toolChoice: { type: 'tool', toolName: 'finalizeResponse' } }
8
}
9
}

小结

  • stopWhen 把 Agent 循环变成一个参数:stepCountIs(N) / hasToolCall(name) / 自定义函数 / 数组组合
  • prepareStep 是 step 之间的强力钩子,能动态改 toolChoice / messages / system
  • result.steps 让你拿到每一步的结构化数据,便于埋点、回放、调试
  • 适用场景是线性多步任务,不需要图结构、HITL、时间旅行
  • 和 LangGraph 的分工:复杂编排交给 LangGraph,LLM 节点内用 AI SDK Agent 循环

本章核心能力部分到这里就结束了。下一篇开始进入前端集成——useChat Hook 替换 Streaming 手写的 useStreamChat