如何构建一个 Agent
每一节都从"我想要某个效果,但当前 chat 做不到"开始,然后新增一个能力。
总览:一个最小 Agent 的调度流程
你会不断循环"下一步决策",在需要信息时调用工具,在需要套路时运行技能,产出后自检,通过后才交付。
用户目标 → State → {下一步?} → TOOL/WRITE/FINAL → 检查 → 交付
从 Chat MVP 开始
需求:先让它能"一问一答"。这一步不是 Agent,但它是后续一切能力的底座。
用户输入 → 模型生成回复 → 输出
export async function chat(message: string) {
const reply = await llm.generate({ input: message })
return reply
}
我想要更可用的输出 → 加结构化输出
缺口:纯文本经常跑偏。解决:强制一个稳定格式,让"聊天"变成"可交付的结构"。
用户输入 → 加输出约束 → 生成 → 结构化结果
type Output = { title: string; bullets: string[]; next: string[] }
export async function chatWithFormat(message: string): Promise<Output> {
const reply = await llm.generate({
input: message,
instructions: [
"请输出 JSON:{ title, bullets[], next[] }",
"bullets 最多 5 条,next 最多 3 条"
]
})
return JSON.parse(reply)
}
我想做复杂任务 → 加计划(Plan)
缺口:复杂任务一次写完容易漏。解决:先拆成 3-6 步可执行步骤,后续逐步补信息与修正。
目标 → 拆成 3-6 步 → 步骤列表
type Plan = { steps: string[] }
export async function makePlan(goal: string): Promise<Plan> {
const text = await llm.generate({
input: goal,
instructions: ["把目标拆成 3-6 个可执行步骤"]
})
return { steps: text.split("\n").filter(Boolean) }
}
我想要外部信息/动作 → 加工具(Tools)
缺口:模型会"凭印象猜"。解决:工具把"查/算/读/写"变成可调用动作,拿到结果再继续推进。
Agent → 调用工具 (参数) → 工具返回结果 → Agent 写回状态
type Tool = {
name: string
run: (input: any) => Promise<any>
}
const tools: Record<string, Tool> = {
weather: { name: "weather", run: async (input) => ({ summary: "周六小雨" }) },
transit: { name: "transit", run: async (input) => ({ summary: "高铁约 30-45 分钟" }) }
}
export async function callTool(name: string, input: any) {
return tools[name].run(input)
}
让它可以持续运行 → 循环(Loop)
缺口:一次调用只能得到一个回复。解决:写一个调度器,每轮决定下一步做什么,直到完成任务。
Decide → Tool → Decide → Write → Check → Done
type State = {
goal: string
plan: string[]
notes: string[]
}
export async function runAgent(goal: string) {
const plan = await makePlan(goal)
const state: State = { goal, plan: plan.steps, notes: [] }
for (let i = 0; i < 8; i++) {
const next = await llm.generate({
input: JSON.stringify(state),
instructions: ["决定下一步:TOOL:xxx / WRITE:xxx / FINAL:xxx"]
})
if (next.startsWith("FINAL:")) return next.slice(6)
if (next.startsWith("TOOL:weather")) {
const obs = await callTool("weather", { city: "苏州" })
state.notes.push(`weather: ${obs.summary}`)
continue
}
if (next.startsWith("WRITE:")) {
state.notes.push(next.slice(6))
continue
}
}
return "未完成:达到最大步数"
}
记住用户偏好 → 记忆(Memory)
缺口:每次都重新问用户同样的问题。解决:读写可复用结论,下次直接加载。
偏好/历史 → 上下文 → 运行 → 写入可复用结论 → 记忆
const memory = { preferences: ["不早起", "不吃辣"] }
export function loadPreferences() {
return memory.preferences
}
export async function runWithMemory(goal: string) {
const prefs = loadPreferences().join("、")
return runAgent(`偏好:${prefs}\n目标:${goal}`)
}
让它更健壮 → 错误处理(Robust)
缺口:工具调用失败时整个流程崩溃。解决:加 try-catch,失败时降级或重试。
调用工具 → {成功?} → 是→继续 / 否→重试或降级
async function safeToolCall(name: string, input: any) {
try {
return await callTool(name, input)
} catch (e) {
return { error: "tool_failed", fallback: "改用已有信息继续" }
}
}