智能体 Hooks
智能体函数可以返回指令,但指令本身用处不大。一个真正的智能体需要工具、技能、子智能体、沙盒和持久化数据来工作。所有智能体功能和资源都来自 Flue 的第二个核心原语——智能体 hooks。
本指南涵盖什么是 hooks、Flue 附带的内置 hooks,以及如何将它们组合成你自己的 hooks。(Flue 新手?请务必先阅读智能体指南。)
什么是智能体 hook?
Hook 是一个普通函数,你在智能体函数体内调用它来为你的智能体赋予一项新能力。你可以通过名称识别 hooks——它们都以 use 开头。每个内置 hook 让你的智能体钩入 Flue 运行时的不同功能:
- 模型(
useModel)选择驱动智能体的 LLM。 - 沙盒(
useSandbox)提供其文件系统和命令执行环境。 - 工具(
useTool)让它调用应用程序代码并影响外部系统。 - MCP 服务器(
useMcpConnection)从开放的 MCP 生态系统挂载工具。 - 技能(
useSkill)提供它可以在需要时加载的专业知识。 - 子智能体(
useSubagent)让它将专注的工作委托给其他智能体。 - 持久化状态(
usePersistentState)在智能体生命周期内保留自定义数据。 - 事件 Hooks(
useAgentStart、useAgentFinish等)在不同生命周期事件上触发逻辑。 - 数据写入器(
useDataWriter)将结构化数据流式传输到你的客户端 UI。 - 自定义 Hooks让你从内置 hooks 组合新的 hooks。
'use agent';
import { useModel, useSandbox, useSkill, useTool } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import { searchIssues } from '../tools/search-issues.ts';
import reviewChecklist from '../skills/review-checklist/SKILL.md';
export function TriageAgent() {
useModel('anthropic/claude-sonnet-4-6');
useSandbox(local());
useTool(searchIssues);
useSkill(reviewChecklist);
return '调查报告的问题并推荐下一步操作。';
}与 React 类似,智能体函数在每个模型调用时_重新渲染_并重新运行其 hooks。与 React 不同的是,资源 hooks 可以有条件地添加和删除。这使得 Flue 能够自动为你管理声明的智能体能力,随着你与智能体的对话演进添加和删除资源(工具、技能、子智能体等):
'use agent';
import { useModel, usePersistentState, useTool } from '@flue/runtime';
import refundTool from '../tools/refund.ts';
export function SupportAgent() {
useModel('anthropic/claude-haiku-4-5');
const [escalated, setEscalated] = usePersistentState('escalated', false);
// 工具可以修改持久化状态。
useTool({
name: 'escalate',
description: '当客户需要退款时升级此对话。',
async run() {
setEscalated(true);
return '已升级。退款工具现在可用。';
},
});
// 如果智能体确定对话需要升级,
// "退款"工具将被解锁并提供给智能体。
if (escalated) {
useTool(refundTool);
}
return '清晰准确地回答客户支持问题。';
}Flue 为你处理这种复杂性,向模型宣布每个变化,以便记录保持连贯。更改工具集可能会使提供商的提示缓存失效(参见条件工具说明)。
每个内置 hook 都在智能体 API参考中有文档记录。
持久化状态
智能体对话可以持续数天或数月,在此期间智能体会学到值得保留的东西:它处于工作流的哪个阶段、它已经检查了什么、它做出的决定。usePersistentState 为这些知识提供了一个持久化的家:
'use agent';
import { useModel, usePersistentState, useTool } from '@flue/runtime';
export function CaseAssistant() {
useModel('anthropic/claude-haiku-4-5');
const [phase, setPhase] = usePersistentState('phase', 'gathering');
const [factsChecked, setFactsChecked] = usePersistentState('factsChecked', 0);
useTool({
name: 'check_fact',
description: '验证一个案例事实。',
async run() {
setFactsChecked((previous) => previous + 1);
},
});
useTool({
name: 'begin_draft',
description: '案例事实验证完成后调用。',
async run() {
setPhase('drafting');
},
});
return `当前阶段:${phase}。已检查事实:${factsChecked}。`;
}签名看起来像 React 的 useState,但值是持久化的:每次写入都记录在对话的存储中,每次渲染都读取最新的值——跨越回合、跨越重启、在对话的生命周期内。值必须是 JSON 可序列化的,每个状态都通过其名称键控。
当下一个值从当前值派生时,传递一个更新函数((previous) => previous + 1)而不是从渲染值计算:渲染值是快照,而更新函数总是看到最新的写入。
智能体知道的其他所有内容都松散地存在于对话记录中;持久化状态是你的代码可以读取和操作的部分。这就是使多步骤行为成为可能的原因:将状态插入到你的指令中,根据它来控制工具和技能(如上面的 SupportAgent 所示),或用它作为守卫使一次性工作恰好发生一次——CaseAssistant 的收集 - 然后 - 起草流程不过是 hooks 读取 phase 值。请参阅持久性了解状态如何存储和恢复。
事件 hooks
事件 hooks 注册在智能体生命周期中的设定时刻运行的回调。
useResponseStart()useAgentStart()每次消息传递给智能体时运行。useAgentFinish()useResponseFinish()
useAgentStart() 是特殊的,因为它是异步的,这使它成为在模型运行之前加载数据的自然位置。这对于将一些加载的初始数据传递给智能体指令中的模型,或根据加载的数据修改能力特别有用。
'use agent';
import { type AgentProps, useAgentStart, useModel, usePersistentState } from '@flue/runtime';
import { crm, type Customer } from '../shared/crm.ts';
export function AccountSupport({ id }: AgentProps) {
useModel('anthropic/claude-haiku-4-5');
const [customer, setCustomer] = usePersistentState<Customer | null>('customer', null);
useAgentStart(async () => {
if (customer) return; // 每个对话只加载一次
setCustomer(await crm.lookupCustomer(id));
});
return customer
? `帮助 ${customer.name}(${customer.plan} 计划)处理他们的账户。`
: '帮助客户处理他们的账户。';
}useResponseStart 和 useResponseFinish 在每个响应的真正开始和真正结束时恰好运行一次(无论智能体接收了多少消息)。从回调返回的任何数据都会合并到响应的元数据上——这是一个信封字段,你的客户端可以在消息内容之外读取——使它们成为标记时间、token 使用量或你自己的应用程序标记的地方:
useResponseStart(() => ({ startedAt: Date.now() }));
useResponseFinish(({ metadata, response }) => ({
elapsed: Date.now() - (metadata.startedAt as number),
totalTokens: response.usage.totalTokens,
}));
事件 hooks 可以像资源一样有条件地声明。它们的回调至少运行一次:完成的工作持久化提交且永不重复,而中断的工作会重试——因此用持久化状态来保护任何不能发生两次的事情(出站电子邮件、页面)。智能体 API记录了每个 hook 的完整细节。
向智能体传递数据
你可以在创建时通过 initialData 向智能体传递结构化数据,并在你的智能体函数内使用 useInitialData() hook 读取它。
'use agent';
import * as v from 'valibot';
import { useModel, useInitialData } from '@flue/runtime';
export function Triage() {
useModel('anthropic/claude-opus-4-6');
const data = useInitialData<v.InferOutput<typeof Triage.initialData>>();
return `端到端处理 GitHub issue #${data!.issue}。`;
}
// 可选:传递 schema 对象以在运行时类型检查初始数据
Triage.initialData = v.object({ issue: v.pipe(v.number(), v.integer()) });
// 分派
await dispatch(Triage, {
id: 'issue-17307',
initialData: { issue: 17307 },
message: '新 GitHub issue 已创建。',
});
# CLI
flue run ./triage-agent.ts --id issue-17307 --data '{"issue": 17307}' \
--message "New GitHub issue created."
# HTTP
curl -X POST https://example.com/agents/triage-agent/issue-17307 \
-H 'Content-Type: application/json' \
-d '{"initialData": {"issue": 17307}, "kind": "user", "body": "New GitHub issue created."}'
initialData schema 静态属性在实例创建时验证数据一次。发送到现有实例的数据会被忽略;记录的值永远不会改变。直接 HTTP 以相同的方式携带它({ "initialData": {…}, "kind": "user", "body": "…" }),client.send({ message, initialData }) 和 flue run --data '<json>' 也是如此。
向客户端流式传输数据
LLM 用文本回复,但客户端通常需要更多结构化数据。这在客户端为用户渲染某些自定义 UI 时特别常见:订单卡片、进度计、图表等。
当你需要在智能体响应旁边写入结构化数据时,使用 useDataWriter() hook。数据写入器声明一个命名的数据通道,并返回一个写入函数,该函数将结构化数据按给定名称传递给客户端:
'use agent';
import { useDataWriter, useModel, useTool } from '@flue/runtime';
import * as v from 'valibot';
import { orders } from '../shared/orders.ts';
export function OrderAssistant() {
useModel('anthropic/claude-haiku-4-5');
const writeOrderCard = useDataWriter('orderCard', {
schema: v.object({ orderId: v.string(), status: v.picklist(['loading', 'loaded']) }),
});
useTool({
name: 'lookup_order',
description: '为客户查询一个订单。',
input: v.object({ orderId: v.string() }),
async run({ data }) {
writeOrderCard({ orderId: data.orderId, status: 'loading' });
const order = await orders.get(data.orderId);
writeOrderCard({ orderId: data.orderId, status: 'loaded' });
return order.summary;
},
});
return '帮助客户检查他们的订单状态。';
}每次写入都被持久化记录并立即流式传输到连接的客户端——工具可以在运行过程中多次写入以驱动实时进度,如上面的 lookup_order 所做的那样。在线路上,每次写入都作为对话消息上的命名数据部分到达({ type: 'data-orderCard', data: … }),与模型写的文本分开。可选的 schema 验证每次写入。
在客户端,数据部分与文本部分在同一消息上到达。使用 @flue/react,渲染它们只是消息部分循环中的一个额外分支:
import { useFlueAgent } from '@flue/react';
import { OrderCard } from './order-card.tsx';
export function OrderChat({ conversationId }: { conversationId: string }) {
const agent = useFlueAgent({ url: `/api/agents/order-assistant/${conversationId}` });
return agent.messages.map((message) =>
message.parts.map((part, index) => {
if (part.type === 'text') return <p key={index}>{part.text}</p>;
if (part.type === 'data-orderCard') {
const order = part.data as { orderId: string; status: string };
return <OrderCard key={index} {...order} />;
}
return null;
}),
);
}通道是严格单向的,从智能体出来:模型永远不会看到数据部分,写入永远不会重新运行智能体函数。无条件地声明数据写入器,每个名称唯一——请参阅智能体 API了解完整细节。
自定义 hooks
因为 hooks 是普通函数调用,你可以将相关的声明提取到命名的、可重用的部分,就像 React 所做的那样:使用自定义 hooks。自定义 hook 是你自己定义的函数,始终以 use 为前缀。它可以接受参数并向其调用者返回值,就像任何其他函数一样:
import { useModel, useTool } from '@flue/runtime';
import { escalateCase } from '../shared/support-tools.ts';
function useEscalation() {
useTool(escalateCase);
return '只有在你确认了账户和问题后才升级给专家。';
}
function SupportAssistant() {
useModel('anthropic/claude-haiku-4-5');
const escalationInstructions = useEscalation();
return `准确回答客户支持问题。${escalationInstructions}`;
}自定义 hooks 是大型智能体保持可读性的方式,也是能力在智能体之间共享的方式——一个捆绑了正确工具、技能和指令的 useGitHub() hook 可以编写一次并放入每个与 GitHub 一起工作的智能体中。