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

第 13 关

人在回路

活由智能体来干;拍板的始终是人。人来审批撤不回的事,在中途纠正它,并在它拿不准时给出回答。
1/33 手风琴褶数:
  • user
  • assistant
  • tool_result
  • tool_result(错误)
游戏厅里的一台抓娃娃机。神谕者坐在算命摊位里,Astor 操作摇杆,而 Tina,也就是“人在回路”里的那个人,隔着玻璃看着。只剩最后一枚硬币。

EventBus

问题

一个没有人看着的智能体,速度很快,直到它做出谁都不想要的事:付错了发票、给所有客户群发了邮件、把最后一枚硬币花在了错误的奖品上。而一个事事都停下来问的智能体,并不比你自己动手更快。

这就是 Autopilot Rex:从不询问,从不等待,从不接电话。它死守第一个计划,无视之后发生的变化,在本该提问的时候瞎猜。

解决方案

在少数几个需要人来判断的地方安排一个人,其余地方都让智能体自己跑。这样的地方有三个,区别在于由谁来打破沉默:

  • 审批

    智能体等待

    在不可撤销的工具运行之前,一个钩子把它将要做的事原原本本地展示给人看,然后等一个“同意”或“拒绝”。

    付款、删除、发给客户的消息、最后一枚硬币:任何撤不回来的事。

  • 中途纠正

    人来打断

    人随时可以把一条纠正放进队列。到下一次工具调用时,循环取消这次调用,改为把纠正交给模型。

    有人在旁边看着,改了主意,或者发现智能体正朝错误的方向走。

  • 上报

    智能体来问

    一个像 ask_human 这样的工具,在人回答之前不会返回。什么时候用它,由模型决定。

    信息不足、两个选项一样好、没有把握。system prompt 写明什么时候该问。

审批和中途纠正都放在第 6 关的 beforeToolExecution 钩子里:它在每个工具之前运行,而且可以等待。审批时,它等待人的回答;中途纠正时,它检查队列里有没有纠正。不管哪种情况,人说的话都会作为工具结果回到模型那里,所以运行会带着新信息继续下去,而不是崩溃。

角色

还是那群熟悉的角色,这次换到了游戏厅里。

算命师 模型
坐在摊位里的神谕者。它阅读历史记录,决定下一次调用。它从不碰那台机器。
摇杆和按钮 工具
move_claw 不花钱,可以撤回。drop_claw 会花掉最后一枚硬币:撤不回来。
Tina 人
她的气泡说明是谁先开口的:! 是她打断,? 是有人问她,YES! 是她同意了。
屏幕 审批
DROP? YES NO:钩子等待时,爪子一动不动。她做出决定之前,什么都不会运行。

在 EventBus 面板里,中途纠正显示为一个 user_steering 事件,后面跟着被取消那次调用的结果。审批和回答没有专门的事件:它们只是一个钩子和一个工具,多花了点时间而已。

代码

使用 astorlm:为不可撤销的工具写一个审批钩子,再用 createSteeringController 包起来,它会加上纠正队列:从你的界面调用 steer(text),纠正就会在下一次工具调用时生效。上报就是一个等待你界面响应的普通工具。

从零手写:在第 2 关循环的工具步骤里加三项检查:队列里的纠正、危险工具的审批,以及一个等待人回应的工具。

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

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

// Your UI: each of these resolves when the person clicks or types.
declare function approve(what: string): Promise<boolean> // shows YES / NO
declare function answer(question: string): Promise<string> // shows a text box

// 1. ESCALATE: a tool the model calls when it isn't sure. It waits for a person.
const askKid = tool({
  name: 'ask_kid',
  description: 'Ask Tina when you are not sure what she wants. Waits for her answer.',
  schema: z.object({ question: z.string() }),
  execute: async ({ question }) => answer(question),
})

// 2. APPROVE: irreversible tools wait for a yes before they run.
const NEEDS_APPROVAL = new Set(['drop_claw'])
const steering = createSteeringController({
  beforeToolExecution: async ({ toolName, input }) => {
    if (!NEEDS_APPROVAL.has(toolName)) return { authorize: true }
    const ok = await approve(`${toolName}(${JSON.stringify(input)})`) // show the real call
    return ok ? { authorize: true } : { authorize: false, mockResult: 'Tina said no. Ask her what to do.' }
  },
})

const agent = await createLocalAgent({
  provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
  systemPrompt: 'You work a claw machine for Tina. If you are not sure which prize she means, ask her before acting.',
  tools: [moveClaw, dropClaw, askKid], // moveClaw, dropClaw: your code
  hooks: steering.hooks, // the steering controller wraps the approval hook
  maxTurns: 20,
})

// 3. STEER: the person can correct the agent at any moment, e.g. from a button.
// The loop cancels the next tool call and hands the model this feedback instead.
onTinaShouts((text) => steering.steer(text)) // your UI: 'No, wait! The penguin!'

const result = await agent.run('Get me the bear!')
console.log(result.content)

注意事项

  • 问得越少,分量越重。只审批不可撤销或昂贵的操作。一个人一小时点二十次 YES,就会不再细看,审批也就沦为走过场。
  • 展示真实的调用。只写“要放下吗?”是不够的。把工具和它的确切参数、金额、收件人、奖品都展示出来,让人批准的正是将要真正运行的东西。
  • 决定没人回答时怎么办。人可能会走开。设一个超时,并选一个安全的默认值:通常是拒绝,并告诉模型原因。
  • 纠正会在下一次工具调用时生效。中途纠正无法让运行到一半的工具停下,也无法让写到一半的回复停在半个词上。如果智能体只是在写文本,纠正就得等着。要强行停下,就中止这次运行。
  • 留下记录。记录谁批准了什么、什么时候、当时看到了什么。出了问题时,“是智能体干的”从来都不是故事的全部。