跳到正文
astorlm
语言: 简体中文
← 地图

第 4 关

何时停止

只要模型还在申请工具,智能体循环就会一直转下去。出口有三个:模型说它完成了,你标记的某个工具关闭了循环,或者轮数预算用完了。只有前两个会给你一个答案。
1/42 手风琴褶数:
  • user
  • assistant
  • tool_result
World 1-1。这一关根本没有星星,但模型还是会一直找下去。时钟就是 maxTurns:这次运行只有 3 轮。

EventBus

问题

第 2 关的循环只有一个正常出口:模型不申请工具就直接回复。但一个找不到目标的模型很少会这么说。它会换一个搜索,再换一种拼写,再换一个。每一圈都要重新发送整段历史记录,所以每一圈都比上一圈更贵。

这就是 Loopboros,永不结束的循环。maxTurns 能拦住它。但再看一眼 World 1-1:时钟走完时,循环并没有失败。它停下来,把最后一条消息交还给你,而那条消息是一个搜索请求。没有答案,也没有错误。

三个出口

  • 旗杆

    end_turn

    World 1-2

    由模型决定。 它不申请任何工具就直接回复。这是正常的出口,大多数运行都应该在这里结束。

    返回:一条以文本形式给出答案的消息。

  • 传送水管

    stopOnToolNames

    World 1-3

    由你的代码决定:当模型调用了你标记过的工具时。 那个工具一旦无错运行完,循环就立刻结束。如果它出错,错误会回到模型那里,循环继续。

    返回:一条带有该工具调用的消息。它的输入就是答案,并且已经过 schema 校验。

  • 时钟

    maxTurns

    World 1-1

    谁都没有决定。预算用完了。 不管模型正在做什么,循环跑满这么多轮就停下。它是一张安全网,不是一个答案。

    返回:最后一条消息是什么就返回什么。往往是一个工具调用。

和 end_turn 相比,终止型工具并不能帮你省下一轮。它给你的是数据形式的答案,经过 schema 校验,你的应用拿来就能用:在这里,就是关卡地图上的一个标记。如果没有 stopOnToolNames,mark_star 运行之后,循环还会再问模型一次,只为了让它说一句“完成”。

代码

使用 astorlm:maxTurns 设置时钟,stopOnToolNames 标记传送水管。run() 总是返回最后一条消息,所以要读一读它,才能知道这次运行走的是哪个出口。

从零手写:第 2 关的循环,三个出口都标了出来。它返回的不是一个光秃秃的字符串,而是这次运行如何结束,这样调用方就不会把超时误当成答案。

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const searchBlocks = tool({
  name: 'search_blocks',
  description: 'Search the ? blocks in one area of the level. Returns what each one hides.',
  schema: z.object({ area: z.string() }),
  execute: async ({ area }) => level.search(area), // your code
})

// The answer as data. Its schema is checked before the loop is allowed to stop.
const markStar = tool({
  name: 'mark_star',
  description: 'Put a marker on the level map where the star is. Call it once, at the end.',
  schema: z.object({ item: z.literal('star'), block: z.number().int().positive() }),
  execute: async ({ block }) => map.addMarker(block), // your code
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [searchBlocks, markStar],
  appendSystemPrompt:
    'When you know where the star is, deliver it with mark_star. ' +
    "If a few searches come back empty, say you couldn't find it.",
  maxTurns: 10, // the clock. Without it, the default is 25.
  stopOnToolNames: ['mark_star'], // the warp pipe
})

const last = await agent.run('Is there a hidden star in this level?')

// The loop hands back its last message whichever way it ended. Read it to find out which.
const call = last.content.find((block) => block.type === 'tool_use')
if (!call) {
  console.log('end_turn:', last.content) // the flag: a plain text answer
} else if (call.name === 'mark_star') {
  console.log('answer:', call.input) // the warp pipe: { item, block }, schema-checked
} else {
  // The clock: maxTurns ran out while the model was still asking for tools.
  throw new Error(`No answer: the run stopped while asking for ${call.name}`)
}

注意事项

  • 一定要设置 maxTurns,并按任务来定大小。一次查询只需要几轮;一次重构可能需要几十轮。astorlm 的默认值是 25。
  • 把时钟走完当成失败来处理。如果运行以一个工具调用结束,就告诉用户你没能完成,或者换一个更清晰的提示词重试。不要给他们看一个空答案。
  • 给模型一个放弃的办法。在 system prompt 里告诉它:搜索一再落空时,就说“我没找到”。被允许停下的模型,会停得更早。
  • 轮数不等于时间。maxTurns 对一轮永远不结束的情况无能为力。那需要超时或者 AbortSignal。