上一章我们看到,AI 智能体由大脑(LLM)·工具·记忆·循环(控制)这 4 个要素组成。本章终于要动手了。目标很简单,就是用最小构成把 4 要素组起来,做出一个"能跑的智能体",并让它真正运行、观察它的行为。花哨的功能先不加。先把"让 LLM 思考、调用工具、把结果回传、再让它思考、完成就停下"这个智能体的心脏部位,用自己的手转起来。

本章目标

"能自己组一个最小的智能体并让它跑起来"

理解循环
能讲清"思考→调用→回传→再思考→停下"这一智能体的一圈
会交付工具
理解工具定义与参数的机制,能给 LLM 配上一件工具
会观察
通过日志看清它在哪判断、在哪失败,衔接到下一章

什么是最小的智能体

"最小的智能体",就是把上一章的 4 要素各自都用最小的形态组起来。不需要豪华的记忆、大量的工具,也不需要多个智能体。像下面这张表那样,先从削到不能再削的构成起步。这里一旦理解了,剩下的无非是把各要素逐个养胖而已。

🧠 大脑

只调用 Claude 一个模型。在提示词里用一行写明角色:"你是一个能使用工具的助手"。

🛠 工具

工具只要一个。搜索或计算都行。先选一个动作确定无误的简单函数。

📝 记忆

一条对话历史数组就够。把往来依次堆上去即可。持久化先不考虑。

🔄 循环

一个 while 就行。"有工具请求就执行,没有就完成"地转。只要事先定好次数上限。

💡 与"一次 API 调用"的差别只有循环。 只向 LLM 问一次拿到答案,那只是普通的 API 利用。一旦在其上加进"执行工具、把结果回传、再让它思考"这样的反复,它就变成了智能体。多出来的实质就是这一个循环。正因如此,先把循环彻底吃透,才是捷径。

智能体循环的内部

智能体的心脏,是一条循环。人类"调查·思考·动手·看结果,不够就重来",把同样的事用程序转起来。先用图把一圈的流程抓住。

STEP 1
让它思考

把至今的对话历史与工具清单交给 LLM。LLM 决定是"直接回答"还是"使用工具"。

STEP 2
调用工具

当 LLM 回复"想用这个工具、用这些参数"时,由我们这边实际去执行那个函数。

STEP 3
回传结果

把执行结果(工具的返回值)追加到对话历史,再次交给 LLM。这里回到 STEP 1。

STEP 4
完成就停下

当 LLM 不再使用工具、给出最终回答时,视为目的达成,跳出循环。

要点在于,STEP 1〜3 绕圈转,到 STEP 4 才第一次跳出。LLM 也许一圈转不完。像"先搜索,看了结果再搜一次"那样,它会转好几圈来逼近目的。用伪代码写出来,会短得惊人。

# 最小的智能体循环(伪代码)
messages = [ user("查一下上个月的平均气温并做摘要") ]

while True:
    reply = llm.think(messages, tools=[search_tool])   # STEP1 思考

    if reply.wants_tool:                              # 有工具请求?
        result = run_tool(reply.tool_name, reply.args) # STEP2 调用
        messages.append(assistant(reply))
        messages.append(tool_result(result))           # STEP3 回传
    else:
        return reply.text                             # STEP4 完成就停下

⚠️ 一定要加次数上限。 上面的伪代码为了说明用了while True,但在真代码里,必须加上"最多转 N 圈"的刹车。LLM 没完没了地不断调用工具的死循环,是现实中经常发生的事故。详见容易踩的坑

给它一个工具

接下来,给 LLM 配上一件工具。这里关键在于,LLM自己无法执行代码。LLM 能做的,只到"想用这个工具、用这些参数"这样用语言提出请求为止。真正去运行函数、把结果返回,永远是我们这边(你的程序)的活儿。

为此,先把工具定义交给 LLM。工具定义,就是"这个工具能做什么、要哪些参数"的说明书。LLM 读了它,来判断何时、如何使用。

① 名称与说明

例:search_weather"传入地名和月份,返回平均气温"。说明文字是 LLM 判断的依据,要写仔细。

② 参数的形态

用 schema(JSON 形式的定义)指定接收哪些参数、什么类型。例:city(字符串)与month(数值)。

③ 实体函数

真正运行的代码本体。接收 LLM 返回的参数,返回结果。这里就是一个普通函数。

这三件套里,诀窍是把①②交给 LLM,③留在自己手边。当 LLM 请求"想用search_weather(city=\"巴黎\", month=6)"时,我们这边接过它的名称和参数,调用③的函数,再把返回值作为工具结果回传到对话里。用伪代码看看。

# ① 与 ② … 交给 LLM 的工具定义
search_tool = {
    "name": "search_weather",
    "description": "传入城市和月份,返回平均气温(℃)",
    "input_schema": {
        "city":  "string",
        "month": "number",
    },
}

# ③ … 留在手边的实体。就是个函数
def run_tool(name, args):
    if name == "search_weather":
        return weather_db.lookup(args["city"], args["month"])
    return "unknown tool"

📊 说明文字是提示词的一部分。 工具的description,是 LLM 判断"何时使用"的唯一线索。写得含糊,它就不用,或在奇怪的场合乱用。诀窍是"把要返回什么、何时该用,当成向人说明那样去写"。工具设计会在下一章的 MCP 里进一步深挖。

实现构想 ― 手写循环与 SDK

把到此为止的部件组起来,第一个智能体就动了。实现大致有两种。要学原理就手写,实务上想省事就用 SDK——两者内部都是同一条循环。先把区别理一理。

A. 手写循环
自己转 while

用自己的 while 转 API 调用和工具执行。里面发生的一切都看得见,最适合学习。控制也随心所欲。

适合场景:想理解原理/想做细粒度控制。

B. SDK 的工具运行器
把循环交出去

向 Claude 系 SDK 等提供的工具执行循环注册工具定义与函数,交给它。省去写定型部分,且稳健。

适合场景:想快点做出来/想把本番的定型处理交出去。

A. 手写循环的全貌,无非是把到此为止的部件老实连起来。持有对话历史,调用 LLM,有工具请求就执行并回传结果,没有就跳出——就这些。

# A. 手写循环(伪代码 / 仿 Claude 的消息风格)
messages = [ user(goal) ]

for step in range(MAX_STEPS):          # ← 用上限做刹车
    reply = client.messages.create(
        model="claude-…",
        messages=messages,
        tools=[search_tool],
    )
    log(step, reply)                       # ← 为观察务必记录

    if reply.stop_reason == "tool_use":
        out = run_tool(reply.tool_name, reply.tool_args)
        messages += [ assistant(reply), tool_result(out) ]
    else:
        break                              # 出了最终回答 → 结束

B. SDK 的工具运行器,这个for循环本身就由库来照看。你要写的只有"工具定义""实体函数""最初的目标"。循环、历史管理、工具结果的回传,都在内部处理。

# B. 交给 SDK 的工具运行器(伪代码)
runner = ToolRunner(
    model="claude-…",
    tools=[search_tool],       # 定义
    handlers={"search_weather": run_tool},  # 实体
)
answer = runner.run(goal)      # 循环在内部转

💡 准确的方法名请查官方文档。 这里的client.messages.createToolRunner都是为传达感觉的伪代码。实际的参数名、返回值的形态、模型 ID 因各家 SDK 而异,且更新频繁,请务必以官方文档确认最新版。先抓概念,细节用文档补齐——这才是不走弯路的做法。按步骤讲解的入门可参考《AI 智能体的做法(入门)》

推荐的做法是"先用 A 亲手写一次,之后用 B 省事"。自己转过一次循环,SDK 就不再是黑盒,卡住时也能想象里面在发生什么。

跑起来并观察

组好就跑。这里重要的是,不是单纯跑起来,而是要观察"它在哪思考了什么、在哪失败了"。智能体开发,正是这份观察的质量分出高下。所以从一开始就埋好日志

至少每一圈记录下面这些。若是手写循环,一行log(step, reply)就够了。

🧭 做了什么决定

这一圈 LLM 是判断"用工具",还是给了最终回答。判断的分岔点。

🛠 用什么参数调用

工具名和参数。这里一歪,结果也就乱。LLM 理解上的偏差,最容易在此暴露。

📦 返回了什么

工具的返回值。太大·为空·报错等,扰乱下一步判断的材料就藏在这里。

真跑起来,会看到一些有趣(也棘手)的行为。比如——LLM 不需要却去调用工具。反过来,该调用的场合不调,只凭记忆就作答。参数里的城市名微妙地写错。本该一次搞定,却把同一个搜索反复做好几遍。这些"判断的怪癖",只有看了日志才第一次察觉。

✅ 日志不是"高级功能",而是从一开始就必备。 智能体内部本就难以看清,没有日志,"它为什么那样动"就完全无从得知。现在用一行print也足够。把这份观察体系化的评估与可观测性,会在第 5 章正式展开。先在这里养成"看"的习惯。

容易踩的坑

即便是最小构成,一跑起来也必然撞上几堵墙。它们都是智能体开发的经典坑,是接下来逐章要解决的。这里先抓住"真面目"和"当下能做的应急处理",正式对策交给后面的章节。

🔁 死循环

没完没了地不断调用同一个工具,迟迟不完成。它判断不出目标已达成,就空转。

应急处理:务必设最大圈数上限。检测到同参数的连续调用就停下。

📦 返回值过大

工具返回巨大的 JSON 或整页全文,把它原样塞回对话,费用和延迟都膨胀。

应急处理:在工具侧只挑必要字段/先摘要再返回。不要把全文原封不动地传。

🎈 上下文膨胀

转的圈越多,对话历史越堆越高,逼近上限,变慢·变贵·变不稳定。

应急处理:把旧的往来摘要压缩/丢掉多余的工具结果。正式对策在后续章节。

📊 这三个都是"上下文设计"的问题。 死循环、返回值肥大、上下文膨胀,根子都落在"给 LLM 传什么、传多少"这一上下文设计上。这套思路被体系化为上下文工程,是智能体开发的脊梁骨。先把"别传太多、别攒太多"当作口号。

本章小结
  • 最小的智能体=把 4 要素组到最小的东西。与一次 API 利用的差别只有循环
  • 循环反复进行"思考→调用工具→回传结果→再思考→完成就停下"这一圈。次数上限的刹车必不可少。
  • 工具是把定义(名称·说明·参数)交给 LLM,实体函数在手边执行。LLM 只负责请求。
  • 实现有手写循环(适合学习)与 SDK 的工具运行器(适合实务)两种,内部相同。
  • 从一开始就用日志观察,找出死循环·返回值肥大·上下文膨胀这些经典坑。

能让一个智能体跑起来之后,接下来轮到正式扩充工具了。下一章《MCP 与工具连接》里,我们会用标准方式(MCP)把工具接到外部服务与数据,一口气拓宽智能体的"手"。想回顾上一章全貌时,请回到第 1 章