☰
从零手搓AI Agent:循环、提示词与工具分发实战
2026/10/11 3:41:52 网站建设 项目流程

1. 从零手搓一个小 Agent:为什么我不建议你直接上大框架

这两年 Agent 这个词被炒得火热,打开任何一个技术社区,满屏都是"智能体""自主规划""工具调用"这类词。但真到动手的时候,很多人第一反应是去拉一个成熟框架,装一堆依赖,跑通一个 Demo,然后……就没有然后了。因为框架把该藏的细节全藏起来了,你根本不知道一个 Agent 到底是怎么"想"的,出了问题也不知道从哪查。

我自己走过这条路。最开始也是拿现成框架搭,能跑,但一旦要改行为逻辑、要控制成本、要排查"为什么它突然不调用工具了",就抓瞎。后来我干脆花了一个周末,参照一个极简 Agent 实现(社区里常被拿来当教学样本的那类,我这边就叫它 pi-agent 思路),自己从零写了一个不到三百行的小 Agent。写完那一刻才真正理解:Agent 的本质没那么玄乎,它就是一个"循环 + 提示词 + 工具分发"的组合体。

这篇东西就是那次折腾的完整复盘。我会把整体设计思路、核心模块拆解、可运行的实操步骤、以及我踩过的坑全部摊开讲。适合两类人:一是想真正搞懂 Agent 内部机制、不想被框架黑盒困住的开发者;二是已经会用框架、但想自己掌控每一行逻辑的中级选手。看完你应该能自己动手写出一个能跑、能调工具、能多轮对话的最小 Agent,并且知道每个参数为什么这么设。

2. 整体设计:一个 Agent 到底由哪几块拼起来

2.1 先想清楚:Agent 和普通聊天机器人的分界线在哪

很多人把"能对话的大模型"和"Agent"混为一谈。区别其实就一条:Agent 能根据当前状态自主决定下一步动作,并且这个动作可以作用于外部世界。普通聊天机器人是你问一句它答一句,被动响应;Agent 是给它一个目标,它自己判断"我现在该查资料、该算数、还是该直接回答",然后执行,拿到结果再判断下一步。

这个"判断—执行—再判断"的过程,落到代码上就是一个循环。循环的每一轮,模型输出一个"意图",程序解析这个意图,如果是调用工具就去调,把结果塞回上下文,再进入下一轮;如果是直接回答,就结束。听起来简单,但魔鬼全在细节里:意图怎么表达、工具怎么注册、结果怎么回填、什么时候该停。

我参照 pi-agent 的思路,把整个 Agent 拆成四个核心模块:对话循环(Loop)、提示词模板(Prompt)、工具注册表(Tool Registry)、消息历史管理(Memory)。下面逐个说。

2.2 为什么选"循环 + 工具分发"而不是"一次性规划"

市面上 Agent 的架构大致分两派:一派是"先规划再执行",让模型一次性输出完整的多步计划,然后按计划走;另一派是"边想边做",每一轮只决定下一步。我选的是后者,也就是 ReAct 那一类的思路。

原因很实际。一次性规划看起来优雅,但模型对长链条的预判能力其实很弱,计划到第三步往往就偏了,而且一旦某步失败,整个计划作废,重规划成本高。边想边做虽然轮次多、token 消耗大一点,但每一步都基于最新的真实结果做决策,容错性强得多。对于一个小 Agent 来说,可控性和容错性比"优雅"重要得多。

提示:如果你做的任务步骤非常固定(比如固定的数据清洗流水线),一次性规划反而更省 token;但只要任务有不确定性,边想边做几乎总是更稳。

2.3 模块之间的数据流长什么样

我用一段话来描述整个数据流,你对照着理解后面代码会轻松很多:用户输入进入 → 拼进消息历史 → 连同系统提示词一起发给模型 → 模型返回要么是普通文本、要么是工具调用请求 → 程序判断类型 → 如果是工具调用,执行对应函数,把返回值作为一条新消息追加到历史 → 再次发给模型 → 重复直到模型返回普通文本或达到最大轮次 → 输出给用户。

这里有个关键设计点:工具调用的结果必须以特定角色(通常是 tool 角色)回填,而不是简单拼成一段文字。因为模型需要明确区分"这是我请求的工具返回"和"这是用户说的话",否则多轮之后它会混乱。这个细节很多手写 Agent 的人第一次都会踩。

3. 核心模块拆解:每一块怎么写才不出坑

3.1 对话循环:整个 Agent 的心脏

循环的骨架大概是这样(用 Python 伪代码示意,实际语言随意):

def run_agent(user_input, max_turns=10): messages.append({"role": "user", "content": user_input}) for turn in range(max_turns): response = call_model(messages, tools=tool_schemas) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: result = execute_tool(call.name, call.arguments) messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result) }) return "达到最大轮次,任务未完成"

这段代码短,但每一行都有讲究。max_turns是必须的,否则模型可能陷入"调工具—不满意—再调"的死循环,烧钱又烧时间。我一般设 8 到 12,具体看任务复杂度。tool_call_id也不能省,它是把工具返回和具体某次调用对应起来的钥匙,多工具并行调用时尤其重要。

还有一个容易忽略的点:每轮都要把模型的原始返回(含 tool_calls)追加进历史,而不是只追加文本内容。因为下一轮模型需要看到"我上一轮请求了什么",才能理解工具返回的是什么。

3.2 提示词模板:决定 Agent 聪明还是智障

系统提示词是 Agent 的"人格说明书",写得好坏直接决定它会不会用工具、用得对不对。我踩过的最大坑就是提示词写得太"客气",模型经常该调工具的时候不调,直接凭记忆瞎答。

一个能用的系统提示词至少要说清三件事:你是谁、你有什么工具、什么时候该用工具。我常用的模板结构是这样的:

你是一个可以调用工具的助手。你可以使用以下工具: {tool_descriptions} 规则: 1. 当问题涉及实时信息、精确计算或你不确定的事实时,必须调用工具,不要凭记忆回答。 2. 一次只调用必要的工具,拿到结果后再决定下一步。 3. 如果工具返回错误,尝试换一种参数或换一个工具,不要重复同样的调用。 4. 当你有足够信息回答用户时,直接给出最终答案,不要再调用工具。

第 1 条和第 4 条是最关键的。第 1 条治"该调不调",第 4 条治"调起来没完"。我实测下来,加上第 4 条之后,无谓的工具调用能减少一大半。

注意:工具描述(tool_descriptions)不是随便写写。模型完全靠这段文字判断工具用途,描述里必须包含"这个工具做什么、参数是什么、什么时候用"。写得含糊,模型就会乱调。

3.3 工具注册表:让 Agent 的手能伸出去

工具注册表的核心是"一份 schema + 一个函数"的映射。schema 给模型看,函数给程序执行。我用一个字典来管理:

TOOLS = { "get_weather": { "schema": { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } }, "func": lambda city: real_weather_api(city) } }

这里有个设计取舍:schema 和函数分开存,还是绑在一起。我选绑在一起,因为改工具的时候不容易漏改另一半。执行时用TOOLS[name]["func"](**args)一行搞定。

参数校验千万别省。模型给的参数经常是字符串形式的数字、或者缺字段,直接传给函数会炸。我在execute_tool里加了一层 try/except,把异常信息作为工具返回内容回填给模型,让它自己纠正。这比程序直接崩溃友好太多。

3.4 消息历史管理:上下文不是越长越好

消息历史是 Agent 的"记忆",但它也是成本大头。每一轮都要把完整历史发给模型,历史越长,token 越贵,而且模型注意力会被稀释,容易忘掉早期关键信息。

我的做法是:保留系统提示词 + 最近 N 轮完整对话 + 更早内容的摘要。N 一般取 6 到 10。摘要可以用模型生成,也可以简单截断。对于小 Agent,我甚至直接用一个滑动窗口,超过就丢最早的,实测对短任务够用。

提示:工具返回的超长内容(比如一整页网页)一定要截断或摘要后再回填,否则一次就能把上下文撑爆。我一般限制单条工具返回不超过 2000 字符。

4. 实操:从零跑通一个能查天气和算数的小 Agent

4.1 环境准备与依赖选择

我用的环境很朴素:Python 3.10+,一个模型 API 的 SDK,加一个 HTTP 请求库。没有用任何 Agent 框架,就是为了看清每一层。模型我选的是支持 function calling 的通用对话模型,因为工具调用能力是 Agent 的命脉,不支持 function calling 的模型得靠提示词硬凑 JSON,稳定性差很多。

依赖清单就三样:模型 SDK、requests、python-dotenv(管理密钥)。装完大概十几秒。密钥放.env文件,别硬编码进代码,这个习惯从第一天就要养成。

4.2 定义两个工具:一个查天气,一个算数

为了演示工具分发,我定义两个差异明显的工具。查天气代表"外部信息获取",算数代表"精确计算"——这两类恰好是模型最容易出错的场景,也最能体现 Agent 的价值。

import math def get_weather(city: str) -> str: # 实际项目里换成真实天气 API fake_db = {"北京": "晴,18度", "上海": "多云,22度"} return fake_db.get(city, f"未找到{city}的天气数据") def calculate(expression: str) -> str: try: # 只允许安全表达式 allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")} result = eval(expression, {"__builtins__": {}}, allowed) return f"计算结果:{result}" except Exception as e: return f"计算失败:{e}"

算数工具用eval有安全风险,我做了两层限制:清空__builtins__,只暴露 math 里的函数。生产环境更稳妥的做法是用专门的表达式解析库,但演示够用了。

4.3 组装主循环并跑通第一个任务

把前面的模块拼起来,主循环大概五十行。跑一个"北京天气怎么样,顺便算一下 23 乘以 47"的任务,你会看到 Agent 先调天气工具,再调算数工具,最后汇总回答。整个过程两到三轮,token 消耗可控。

这里有个实测细节:两个工具调用有时会并行返回(模型一次返回多个 tool_calls),有时会串行。你的循环必须两种都能处理。我一开始只处理了单个调用,结果遇到并行调用直接漏掉一个,排查了半天。

4.4 参数计算:max_turns 和温度怎么定

max_turns我前面说 8 到 12,具体怎么定?我的经验公式是:预估任务最大步数 × 1.5。比如一个任务最多需要查 3 次资料、算 2 次,那就是 5 步,乘 1.5 取 8。留余量是因为模型偶尔会走弯路。

温度(temperature)对 Agent 影响很大。工具调用场景我一般设 0 到 0.3,越低越稳定,模型更倾向于按规则走。设高了它会"发挥创意",该调工具的时候跟你聊天。只有做创意类任务时才调高。

参数推荐值说明
max_turns8-12任务步数 × 1.5
temperature0-0.3工具场景求稳
单条工具返回上限2000 字符防上下文爆炸
历史保留轮数6-10平衡成本与记忆

5. 常见问题与排查技巧实录

5.1 模型死活不调用工具怎么办

这是最高频的问题。排查顺序我总结成三步:先看工具描述是不是太模糊,模型看不懂自然不会用;再看系统提示词有没有明确"必须调用"的规则;最后看模型本身是否支持 function calling。我遇到过描述里写"获取信息"这种含糊词,改成"查询指定城市的实时天气,返回温度和天气状况"之后,调用率立刻上来了。

5.2 工具调用陷入死循环怎么破

表现是模型反复调同一个工具、传同样的参数。原因通常是工具返回了错误但模型没理解,或者提示词没告诉它"拿到结果就停"。解法有两个:一是max_turns兜底,二是把错误信息写清楚,让模型知道"这条路走不通"。我在工具返回里会明确写"错误:参数 city 不能为空",而不是抛一个裸异常。

5.3 上下文越来越长、越来越贵

前面提过滑动窗口和截断。补充一个技巧:把工具返回的原始 JSON 精简后再回填。比如天气 API 返回一大坨,我只提取温度和天气两个字段拼成一句话回填。这样既省 token,模型也更容易抓重点。

5.4 常见问题速查表

现象可能原因解决方向
不调用工具描述模糊/提示词没要求改描述、加规则
死循环无 max_turns/错误信息不清加轮次上限、明确错误
上下文爆炸工具返回过长截断、摘要、精简字段
参数报错模型给错类型加校验、异常回填
答非所问历史混乱检查角色标记是否正确

提示:调试 Agent 最有效的手段是把每一轮的完整 messages 打印出来。你会直观看到模型"看到"了什么、"想"了什么,问题一目了然。我调试时几乎全程开着这个日志。

6. 我踩过的几个坑和一点个人体会

第一个坑是把工具结果拼成普通文本回填。早期我图省事,把工具返回直接拼进 assistant 的消息里,结果模型分不清哪些是自己说的、哪些是工具给的,多轮之后开始胡编。改成独立的 tool 角色消息后,问题消失。

第二个坑是忘了处理并行工具调用。模型一次返回多个 tool_calls 时,我最初只取了第一个,导致任务信息缺失。后来改成遍历所有调用、逐个执行、逐个回填,才稳定下来。

第三个坑是提示词里没写"够了就停"。模型拿到工具结果后,有时会想"要不再确认一下",于是反复调用。加上"信息足够时直接回答"这条规则后,轮次明显下降。

我个人在实际操作中的体会是:写 Agent 最难的从来不是代码,而是把"什么时候该做什么"用自然语言给模型讲清楚。代码只是骨架,提示词才是灵魂。你花在打磨提示词上的时间,往往比写循环本身多得多。所以别急着堆功能,先把一个工具、一条规则调稳,再往上加。一个小而稳的 Agent,价值远大于一个大而乱的。

后续如果想扩展,我建议按这个顺序加:先加"工具调用失败重试",再加"多轮记忆摘要",最后才考虑"多 Agent 协作"。每一步都跑稳了再走下一步,这是我折腾下来最实在的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询