上一章我们看到,AI 智能体由大脑(LLM)·工具·记忆·循环(控制)这 4 个要素组成。本章终于要动手了。目标很简单,就是用最小构成把 4 要素组起来,做出一个"能跑的智能体",并让它真正运行、观察它的行为。花哨的功能先不加。先把"让 LLM 思考、调用工具、把结果回传、再让它思考、完成就停下"这个智能体的心脏部位,用自己的手转起来。
"能自己组一个最小的智能体并让它跑起来"
什么是最小的智能体
"最小的智能体",就是把上一章的 4 要素各自都用最小的形态组起来。不需要豪华的记忆、大量的工具,也不需要多个智能体。像下面这张表那样,先从削到不能再削的构成起步。这里一旦理解了,剩下的无非是把各要素逐个养胖而已。
只调用 Claude 一个模型。在提示词里用一行写明角色:"你是一个能使用工具的助手"。
工具只要一个。搜索或计算都行。先选一个动作确定无误的简单函数。
一条对话历史数组就够。把往来依次堆上去即可。持久化先不考虑。
一个 while 就行。"有工具请求就执行,没有就完成"地转。只要事先定好次数上限。
💡 与"一次 API 调用"的差别只有循环。 只向 LLM 问一次拿到答案,那只是普通的 API 利用。一旦在其上加进"执行工具、把结果回传、再让它思考"这样的反复,它就变成了智能体。多出来的实质就是这一个循环。正因如此,先把循环彻底吃透,才是捷径。
智能体循环的内部
智能体的心脏,是一条循环。人类"调查·思考·动手·看结果,不够就重来",把同样的事用程序转起来。先用图把一圈的流程抓住。
把至今的对话历史与工具清单交给 LLM。LLM 决定是"直接回答"还是"使用工具"。
当 LLM 回复"想用这个工具、用这些参数"时,由我们这边实际去执行那个函数。
把执行结果(工具的返回值)追加到对话历史,再次交给 LLM。这里回到 STEP 1。
当 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——两者内部都是同一条循环。先把区别理一理。
用自己的 while 转 API 调用和工具执行。里面发生的一切都看得见,最适合学习。控制也随心所欲。
适合场景:想理解原理/想做细粒度控制。
向 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.create和ToolRunner都是为传达感觉的伪代码。实际的参数名、返回值的形态、模型 ID 因各家 SDK 而异,且更新频繁,请务必以官方文档确认最新版。先抓概念,细节用文档补齐——这才是不走弯路的做法。按步骤讲解的入门可参考《AI 智能体的做法(入门)》。
推荐的做法是"先用 A 亲手写一次,之后用 B 省事"。自己转过一次循环,SDK 就不再是黑盒,卡住时也能想象里面在发生什么。
跑起来并观察
组好就跑。这里重要的是,不是单纯跑起来,而是要观察"它在哪思考了什么、在哪失败了"。智能体开发,正是这份观察的质量分出高下。所以从一开始就埋好日志。
至少每一圈记录下面这些。若是手写循环,一行log(step, reply)就够了。
这一圈 LLM 是判断"用工具",还是给了最终回答。判断的分岔点。
工具名和参数。这里一歪,结果也就乱。LLM 理解上的偏差,最容易在此暴露。
工具的返回值。太大·为空·报错等,扰乱下一步判断的材料就藏在这里。
真跑起来,会看到一些有趣(也棘手)的行为。比如——LLM 不需要却去调用工具。反过来,该调用的场合不调,只凭记忆就作答。参数里的城市名微妙地写错。本该一次搞定,却把同一个搜索反复做好几遍。这些"判断的怪癖",只有看了日志才第一次察觉。
✅ 日志不是"高级功能",而是从一开始就必备。 智能体内部本就难以看清,没有日志,"它为什么那样动"就完全无从得知。现在用一行print也足够。把这份观察体系化的评估与可观测性,会在第 5 章正式展开。先在这里养成"看"的习惯。
容易踩的坑
即便是最小构成,一跑起来也必然撞上几堵墙。它们都是智能体开发的经典坑,是接下来逐章要解决的。这里先抓住"真面目"和"当下能做的应急处理",正式对策交给后面的章节。
没完没了地不断调用同一个工具,迟迟不完成。它判断不出目标已达成,就空转。
应急处理:务必设最大圈数上限。检测到同参数的连续调用就停下。
工具返回巨大的 JSON 或整页全文,把它原样塞回对话,费用和延迟都膨胀。
应急处理:在工具侧只挑必要字段/先摘要再返回。不要把全文原封不动地传。
转的圈越多,对话历史越堆越高,逼近上限,变慢·变贵·变不稳定。
应急处理:把旧的往来摘要压缩/丢掉多余的工具结果。正式对策在后续章节。
📊 这三个都是"上下文设计"的问题。 死循环、返回值肥大、上下文膨胀,根子都落在"给 LLM 传什么、传多少"这一上下文设计上。这套思路被体系化为上下文工程,是智能体开发的脊梁骨。先把"别传太多、别攒太多"当作口号。
- 最小的智能体=把 4 要素组到最小的东西。与一次 API 利用的差别只有循环。
- 循环反复进行"思考→调用工具→回传结果→再思考→完成就停下"这一圈。次数上限的刹车必不可少。
- 工具是把定义(名称·说明·参数)交给 LLM,实体函数在手边执行。LLM 只负责请求。
- 实现有手写循环(适合学习)与 SDK 的工具运行器(适合实务)两种,内部相同。
- 从一开始就用日志观察,找出死循环·返回值肥大·上下文膨胀这些经典坑。
能让一个智能体跑起来之后,接下来轮到正式扩充工具了。下一章《MCP 与工具连接》里,我们会用标准方式(MCP)把工具接到外部服务与数据,一口气拓宽智能体的"手"。想回顾上一章全貌时,请回到第 1 章。