第 1 关
什么是智能体?
几乎任何用到语言模型的东西都会被叫作“智能体”。有用的定义要窄得多,归结起来只有一个问题:下一步由谁决定?在智能体里,由模型决定。
1/16
手风琴褶数:
- user
- assistant
- tool_result
EventBus
问题
一个聊天机器人、一个调用两次模型的脚本、一个能自己修 bug 的系统,都被叫作智能体。这让人很难弄清自己到底在构建什么,更难为手头的工作选对工具。
这段动画是一个小小的冒险游戏。一位村民问:“我把村里宝箱的钥匙弄丢了。它在哪儿?”游戏里有两个函数能帮上忙:ask_villager
向某人打听他看到了什么,search_area 在地图上的某个地点搜索。注意看:是谁决定运行哪一个,又是在什么时候。
是什么让它成为智能体
智能体不是“带工具的 LLM”,也不是“聪明的工作流”。它是一个由模型来选择控制流的系统:调用哪个工具、按什么顺序、什么时候停下。你的代码从来不会写“先问渔夫,再搜橡树”。模型读完每个结果,再决定下一步。
让这一切成为可能的循环,就是下一关的内容。
动画里的角色
- 村民 你的应用
- 问题从村里的一所房子出发,答案也回到那里。
- Astor 循环
- 智能体循环,背着装满消息的班多钮手风琴。神谕者的纸条让他去哪儿他就去哪儿,别的地方一概不去。
- 神谕者 LLM
- 模型,待在两堆篝火之间的洞穴里。它从不出门:只知道手风琴里装着的东西。没有工具时,它只能动嘴,就像惰者 Petrus。
- 码头和树林 工具
ask_villager和search_area:你自己的函数。一屏地图会一直黑着,直到有路通向它。- 被点亮的屏幕 控制流
- 整个模式浓缩在一张图里,小地图上也有。每条路只在神谕者读完上一个结果、申请调用对应工具时才会出现。如果是工作流,你的代码会在任何人提问之前就把所有的路画好。山一直是黑的:模型从来不需要它。
- 卢比和红心 tokens、maxTurns
- 每次拜访神谕者都要花卢比,因为整个手风琴都要重新读一遍,还要花掉轮数预算里的一颗红心。数字仅作示意。
代码
使用 astorlm:用 tool() 包装你自己的函数,再交给智能体。循环是内置的:调用哪些工具、按什么顺序、什么时候信息足够回答,都由模型来选。
从零手写:把你游戏里的函数作为工具交给模型。循环本身就是第 2 关的那个;唯一的新东西是它拿到了哪些工具。
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'
// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const 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
})
// Your own game functions, wrapped as tools: a name, a description and an input schema.
const askVillager = tool({
name: 'ask_villager',
description: 'Ask someone in the village what they saw. Returns what they say.',
schema: z.object({ name: z.string().describe('Who to ask, e.g. "fisher" or "baker"') }),
execute: async ({ name }) => world.villager(name).say(),
})
const searchArea = tool({
name: 'search_area',
description: 'Search one spot on the map. Returns what is found there, if anything.',
schema: z.object({ area: z.string().describe('A named spot, e.g. "old oak" or "bridge"') }),
execute: async ({ area }) => world.search(area),
})
// The model decides which tools to call, in what order, and when to stop.
const agent = await createLocalAgent({
provider,
tools: [askVillager, searchArea],
maxTurns: 5, // a cap on the laps, in case it never settles
})
await agent.run('I lost the key to the village chest. Where is it?') // "In the crow's nest on the old oak"
// An agent that finds a villager's lost key. Plain TypeScript, no SDK.
// Your game. In a real one these read the world state and the characters' dialogue.
async function askVillager(name: string) {
const seen: Record<string, string> = {
fisher: 'A crow flew off with something shiny, toward the old oak in the woods.',
}
return seen[name] ?? `The ${name} saw nothing.`
}
async function searchArea(area: string) {
return area === 'old oak' ? "In the crow's nest: a small brass key." : `Nothing at the ${area}.`
}
// The same functions, as tools the model can ask for. Each one returns text.
const tools = {
ask_villager: ({ name }: { name: string }) => askVillager(name),
search_area: ({ area }: { area: string }) => searchArea(area),
}
// runAgent is the loop from level 2, with the tools passed in. Your code never says
// "ask the fisher, then search the oak": the model picks each step after reading the last result.
export async function agent(question: string): Promise<string> {
return runAgent(question, tools)
}
# An agent that finds a villager's lost key. Standard library only, no SDK.
# Your game. In a real one these read the world state and the characters' dialogue.
def ask_villager(name):
seen = {"fisher": "A crow flew off with something shiny, toward the old oak in the woods."}
return seen.get(name, f"The {name} saw nothing.")
def search_area(area):
return "In the crow's nest: a small brass key." if area == "old oak" else f"Nothing at the {area}."
# The same functions, as tools the model can ask for. Each one returns text.
TOOLS = {
"ask_villager": lambda args: ask_villager(args["name"]),
"search_area": lambda args: search_area(args["area"]),
}
# run_agent is the loop from level 2, with the tools passed in. Your code never says
# "ask the fisher, then search the oak": the model picks each step after reading the last result.
def agent(question):
return run_agent(question, TOOLS)
何时使用,以及代价是什么
- 当你没法事先把步骤写下来时,就用它。钥匙可能在鸟窝里、在桥底下,或者已经被卖到了商店:写成代码的话,每种新情况都是又一个分支。只要有工具,智能体用同一个循环就能应付它们。
-
每一步都是一次模型调用。这里两个工具意味着三次调用。步骤越多,延迟越高、token 越多,所以要用
maxTurns限制圈数。 - 同一个问题可能走出不同的路径。把模型选择的路径记录下来,这样你才能看清一个答案为什么会是这样。
- 工具就是边界。模型只能做你的工具允许的事。你交给它什么,它就可能弄坏什么。