> Agent Harness Patterns 第 1 关，一条讲解 AI 智能体工作原理的模式路线。网页版：https://harnesspatterns.dev/zh/patterns/what-is-an-agent · 全部模式（英文）：https://harnesspatterns.dev/llms.txt

# 什么是智能体？

几乎任何用到语言模型的东西都会被叫作“智能体”。有用的定义要窄得多，归结起来只有一个问题：下一步由谁决定？在智能体里，由模型决定。

## 问题

一个聊天机器人、一个调用两次模型的脚本、一个能自己修 bug 的系统，都被叫作智能体。这让人很难弄清自己到底在构建什么，更难为手头的工作选对工具。

这段动画是一个小小的冒险游戏。一位村民问：“我把村里宝箱的钥匙弄丢了。它在哪儿？”游戏里有两个函数能帮上忙：`ask_villager` 向某人打听他看到了什么，`search_area` 在地图上的某个地点搜索。注意看：是谁决定运行哪一个，又是在什么时候。

## 是什么让它成为智能体

智能体不是“带工具的 LLM”，也不是“聪明的工作流”。它是一个由**模型来选择控制流**的系统：调用哪个工具、按什么顺序、什么时候停下。你的代码从来不会写“先问渔夫，再搜橡树”。模型读完每个结果，再决定下一步。

让这一切成为可能的循环，就是下一关的内容。

## 动画里的角色

- **村民** (你的应用): 问题从村里的一所房子出发，答案也回到那里。
- **Astor** (循环): 智能体循环，背着装满消息的班多钮手风琴。神谕者的纸条让他去哪儿他就去哪儿，别的地方一概不去。
- **神谕者** (LLM): 模型，待在两堆篝火之间的洞穴里。它从不出门：只知道手风琴里装着的东西。没有工具时，它只能动嘴，就像惰者 Petrus。
- **码头和树林** (工具): `ask_villager` 和 `search_area`：你自己的函数。一屏地图会一直黑着，直到有路通向它。
- **被点亮的屏幕** (控制流): 整个模式浓缩在一张图里，小地图上也有。每条路只在神谕者读完上一个结果、申请调用对应工具时才会出现。如果是工作流，你的代码会在任何人提问之前就把所有的路画好。山一直是黑的：模型从来不需要它。
- **卢比和红心** (tokens、maxTurns): 每次拜访神谕者都要花卢比，因为整个手风琴都要重新读一遍，还要花掉轮数预算里的一颗红心。数字仅作示意。

## 代码

**使用 astorlm：**用 `tool()` 包装你自己的函数，再交给智能体。循环是内置的：调用哪些工具、按什么顺序、什么时候信息足够回答，都由模型来选。

**从零手写：**把你游戏里的函数作为工具交给模型。循环本身就是第 2 关的那个；唯一的新东西是它拿到了哪些工具。

**使用 astorlm**

```ts
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"
```

**TypeScript**

```ts
// 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)
}
```

**Python**

```python
# 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` 限制圈数。
- **同一个问题可能走出不同的路径。**把模型选择的路径记录下来，这样你才能看清一个答案为什么会是这样。
- **工具就是边界。**模型只能做你的工具允许的事。你交给它什么，它就可能弄坏什么。

## 相关模式

- [2 · 智能体循环](https://harnesspatterns.dev/zh/patterns/agent-loop.md)
- [3 · 设计一个工具](https://harnesspatterns.dev/zh/patterns/designing-a-tool.md)
- [12 · 先规划，再反思](https://harnesspatterns.dev/zh/patterns/plan-and-reflect.md)
