简介:这份资源是字节跳动开源的Agent实践手册配套可运行源码包,面向企业内部业务与技术团队、AI应用开发者及希望将智能体技术落地到实际业务中的技术人员。手册系统总结了字节在飞书、抖音、教育等多条业务线落地Agent的方法、工具与规范,涵盖核心技术组件、开发流程、应用场景、运营优化、安全合规、风险应对、团队协作及全球化适配等内容,源码包则提供了可直接运行的配套示例,帮助读者从0到1理解企业级AI智能体的搭建思路。压缩包共3个文件,以html页面、inscode配置和gitignore忽略规则为主,整体约6KB,体量轻便,便于快速浏览与本地运行。目前已有824人学习下载,适合想了解Agent工程化落地路径、参考真实业务案例并动手实践的技术人员。1. 从一份可运行源码说起:Agent 手册到底解决了什么很多人第一次接触 Agent 开发,卡住的地方不是模型调用,而是“模型会说话,但不会干活”。你让它查天气,它编一个温度;你让它读文件,它给你一段伪代码。问题的根子在于:大模型本身只是一个文本生成器,它没有手也没有脚。所谓 Agent,就是给模型装上手脚的那套调度框架——把模型输出解析成工具调用,把工具执行结果塞回上下文,循环往复直到任务完成。这份“字节开源 Agent 手册”附带可运行源码,价值就在这里:它不是又一篇讲 ReAct 是什么的概念文,而是一套能跑起来的骨架。你拿到手,改几个配置就能接自己的模型、注册自己的工具、跑通一个完整的“思考—行动—观察”闭环。适合两类人:一是想从零理解 Agent 内部调度逻辑的开发者,二是有具体业务场景(比如自动查数据库、自动整理文件)想快速搭原型的工程师。下面我按自己复现这套东西的顺序,把关键环节拆开讲。2. 拆开 Agent 手册的骨架:调度循环与工具注册怎么落地2.1 先看清一个最小 Agent 的三个组成部分不管手册里封装了多少层,一个能跑的 Agent 核心就三块:模型客户端、工具注册表、调度循环。模型客户端负责发请求拿回复;工具注册表维护“工具名 → 函数 → 参数描述”的映射;调度循环负责把用户输入、历史消息、工具定义拼成 prompt,发给模型,解析回复,决定是继续调工具还是返回最终答案。我一般会先画一张数据流:用户输入 → 拼 prompt → 模型返回 → 解析出工具调用 → 执行工具 → 结果追加到消息列表 → 再次拼 prompt → …… → 模型返回纯文本 → 结束。这个循环的退出条件很关键,常见做法是判断模型回复里有没有工具调用标记,没有就认为它给出了最终答案。手册源码里通常用一个max_iterations兜底,防止模型陷入死循环。2.2 用 Python 跑通第一个工具调用闭环下面这段代码是我从手册骨架里抽出来的最小可运行版本,去掉了具体业务工具,只留一个计算器,方便你先把闭环跑通再往里加东西。import json import re # 1. 工具注册表:工具名 -> (函数, 描述, 参数schema) TOOLS = {} def register_tool(name, description, parameters): def decorator(func): TOOLS[name] = { "function": func, "description": description, "parameters": parameters, } return func return decorator @register_tool( name="calculator", description="执行四则运算,输入一个数学表达式字符串", parameters={"expression": "string, 例如 '3 * (4 + 5)'"}, ) def calculator(expression: str) -> str: # 注意:生产环境不要直接用 eval,这里仅为演示 try: result = eval(expression, {"__builtins__": {}}, {}) return str(result) except Exception as e: return f"计算失败: {e}" # 2. 把工具注册表转成模型能看懂的描述文本 def build_tool_prompt(): lines = [] for name, meta in TOOLS.items(): lines.append(f"- {name}: {meta['description']} 参数: {meta['parameters']}") return "\n".join(lines) # 3. 模拟模型返回(真实场景替换为 API 调用) def fake_llm(messages): last = messages[-1]["content"] if "计算" in last and "Action:" not in last: return "我需要用计算器。\nAction: calculator\nAction Input: 12 * 8 + 5" return "最终答案:计算结果是 101" # 4. 调度循环 def run_agent(user_input, max_iterations=5): messages = [{"role": "user", "content": user_input}] for i in range(max_iterations): prompt = ( f"你可以使用以下工具:\n{build_tool_prompt()}\n" f"请用 Action/Action Input 格式调用工具,或直接给出最终答案。\n" f"对话历史:\n{json.dumps(messages, ensure_ascii=False)}" ) reply = fake_llm([{"role": "user", "content": prompt}]) print(f"[第{i+1}轮模型回复] {reply}") action_match = re.search(r"Action:\s*(\w+)", reply) input_match = re.search(r"Action Input:\s*(.+)", reply) if action_match and input_match: tool_name = action_match.group(1).strip() tool_input = input_match.group(1).strip() if tool_name in TOOLS: observation = TOOLS[tool_name]["function"](tool_input) messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"Observation: {observation}"}) else: messages.append({"role": "user", "content": f"Observation: 未知工具 {tool_name}"}) else: return reply return "达到最大迭代次数,任务未完成" if __name__ == "__main__": print(run_agent("帮我算一下 12 乘以 8 再加 5 等于多少"))这段代码的逻辑说明:register_tool装饰器把函数和它的元信息塞进全局字典,这是手册里最常见的工具注册方式,好处是新增工具不用改调度循环。build_tool_prompt把工具描述拼成文本,真实场景里这部分会作为 system prompt 的一部分发给模型。调度循环里用正则解析Action和Action Input,这是 ReAct 风格的经典解析方式,简单但够用。参数说明:max_iterations建议设 5 到 10,太小复杂任务跑不完,太大模型抽风时会浪费 token。eval那里务必换成安全的表达式解析库,我这里只是为了让你一眼看懂工具函数长什么样。跑通之后你会看到两轮循环:第一轮模型决定调计算器,第二轮拿到结果后给出最终答案。2.3 工具描述写得好不好,直接决定模型调不调得对这是血泪经验:很多人跑不通 Agent,不是循环写错了,是工具描述太潦草。模型只能通过你给的文字判断该不该调这个工具。描述里要写清楚三件事——这个工具干什么、什么时候用、参数长什么样。比如“查询数据库”这种描述就太模糊,模型不知道是查用户表还是订单表。改成“根据用户 ID 查询该用户的订单列表,输入为整数用户 ID”,命中率立刻不一样。手册源码里通常会给每个工具配一个 JSON Schema 风格的参数定义,即使你用的是纯文本 prompt,也建议把参数类型和示例写进去。我一般会在描述末尾加一句“如果参数缺失,请先向用户追问”,能减少模型瞎编参数的情况。3. 把手册接到真实模型和真实工具上:配置与改造清单3.1 替换模型客户端时要注意的三个接口差异手册里的模型调用层通常做了抽象,但不同厂商的接口在三个地方有差异:消息格式、工具调用返回结构、流式输出。消息格式上,有的要求role只能是 system/user/assistant,有的允许 tool 角色;工具调用返回结构上,有的把工具调用放在content里让你自己解析,有的直接返回结构化的tool_calls字段。我一般会写一个适配层,把不同厂商的返回统一成内部格式。下面是一个适配函数的骨架:def normalize_response(raw_response): """ 把不同厂商的模型返回统一成: {"type": "tool_call", "tool": "xxx", "input": "..."} 或 {"type": "final", "content": "..."} """ # 情况一:结构化 tool_calls 字段 if hasattr(raw_response, "tool_calls") and raw_response.tool_calls: call = raw_response.tool_calls[0] return { "type": "tool_call", "tool": call.function.name, "input": call.function.arguments, } # 情况二:纯文本里用正则抠 Action text = raw_response.content if hasattr(raw_response, "content") else str(raw_response) action_match = re.search(r"Action:\s*(\w+)", text) input_match = re.search(r"Action Input:\s*(.+)", text) if action_match and input_match: return { "type": "tool_call", "tool": action_match.group(1).strip(), "input": input_match.group(1).strip(), } return {"type": "final", "content": text}逻辑说明:这个函数把两种主流返回形态收敛成同一种内部事件,调度循环只需要处理tool_call和final两种类型。参数上,raw_response可能是对象也可能是字符串,所以做了hasattr判断。改造时把调度循环里的解析逻辑换成调用这个函数即可,循环本身不用动。3.2 注册一个真实工具:从文件读取到结果回填拿“读取本地文本文件并统计行数”举例,这是最不容易翻车的真实工具,适合用来验证整条链路。@register_tool( name="count_lines", description="读取指定路径的文本文件,返回文件总行数。输入为文件绝对路径字符串。", parameters={"path": "string, 例如 '/data/logs/app.txt'"}, ) def count_lines(path: str) -> str: import os if not os.path.isfile(path): return f"文件不存在: {path}" try: with open(path, "r", encoding="utf-8") as f: lines = f.readlines() return f"文件共 {len(lines)} 行" except Exception as e: return f"读取失败: {e}"逻辑说明:工具函数必须自己处理异常并返回字符串,因为调度循环只负责把返回值塞回上下文,不会帮你捕获异常。参数说明:路径建议要求绝对路径,相对路径在不同工作目录下行为不一致,是常见翻车点。注册完之后,在build_tool_prompt里会自动带上这个工具的描述,模型就能在需要时调用它。3.3 用配置文件管理模型参数和工具开关手册源码里一般会有一个配置文件,把模型名、温度、最大 token、启用的工具列表放进去。我建议至少把这三类参数外置:模型连接参数(base_url、api_key、model_name)、生成参数(temperature、max_tokens)、调度参数(max_iterations、工具白名单)。这样换环境不用改代码,也方便做 A/B 对比。一个简单的 YAML 配置示例:model: base_url: "https://your-endpoint/v1" model_name: "your-model" temperature: 0.2 max_tokens: 1024 agent: max_iterations: 8 enabled_tools: - calculator - count_lines温度建议设低一点,0.1 到 0.3 之间,Agent 任务需要稳定复现,高温会让模型在工具调用格式上自由发挥,解析失败率飙升。enabled_tools用来做工具白名单,工具多了之后模型容易选错,按场景裁剪比全量注册更可靠。4. 避坑与排查:Agent 跑不通时先看这五条4.1 模型一直不调工具,只给文字答案现象:循环第一轮模型就返回“最终答案”,根本没走工具调用。原因通常是工具描述没写清楚,或者 prompt 里没有明确要求“需要外部信息时必须调用工具”。解决:在 system prompt 里加一句“如果你不确定答案,必须调用工具获取信息,不要凭记忆回答”,同时检查工具描述是否包含具体使用场景。4.2 工具调用格式解析失败,循环空转现象:模型明明输出了工具名,但正则没匹配上,调度循环认为它给了最终答案。原因是不同模型输出的格式有细微差异,比如Action:后面多了空格、换行,或者用了中文冒号。解决:正则里用\s*容忍空白,同时把Action[::]写成兼容中英文冒号。更稳妥的做法是优先使用模型厂商的结构化工具调用字段,正则只作为兜底。4.3 工具执行结果太长,把上下文撑爆现象:调用一个查询工具返回了几千行数据,下一轮模型直接报超长错误。原因是工具函数没有对返回值做截断。解决:在工具函数里限制返回长度,比如只返回前 2000 字符,并加一句“结果已截断,如需完整数据请缩小查询范围”。调度循环里也可以加一个统一的返回值截断逻辑。4.4 多轮之后模型忘记原始任务现象:循环到第三四轮,模型开始答非所问。原因是消息列表越来越长,早期用户输入被淹没。解决:在每轮拼 prompt 时,把原始用户任务单独放在最前面并加粗标记,或者维护一个“任务摘要”字段,每轮都带上。手册源码里如果有消息裁剪逻辑,检查它是不是把最早的 user 消息裁掉了。4.5 工具函数抛异常导致整个循环崩溃现象:某个工具遇到异常输入直接抛出未捕获异常,程序退出。原因是工具函数没有做 try/except。解决:所有工具函数内部必须捕获异常并返回错误字符串,让模型看到错误信息后有机会自我修正。调度循环外层也建议包一层 try/except,记录日志后继续下一轮,而不是直接崩掉。5. 让 Agent 更稳的一个技巧:给工具调用加一层结果校验跑通基本闭环之后,你会发现最大的不稳定来源不是模型能力,而是模型“以为”工具执行成功了。比如它调了计算器,工具返回“计算失败: invalid syntax”,但模型下一轮仍然基于一个错误结果往下推理。我一般会在调度循环里加一层轻量校验:工具返回后,检查返回值里有没有“失败”“错误”“不存在”这类关键词,如果有,就在 Observation 前面加一句“工具执行未成功,请检查参数后重试”。这个技巧不需要改模型,也不需要复杂的框架,就是在回填 Observation 的地方多几行判断:ERROR_KEYWORDS = ["失败", "错误", "不存在", "异常", "invalid"] def wrap_observation(tool_name, raw_result): lowered = raw_result.lower() if any(kw in lowered for kw in ERROR_KEYWORDS): return ( f"工具 {tool_name} 执行未成功,返回信息:{raw_result}\n" f"请检查输入参数是否正确,或换一种方式重试。" ) return f"工具 {tool_name} 执行成功,返回:{raw_result}"逻辑说明:ERROR_KEYWORDS是可扩展的,你可以按自己工具的实际错误文案往里加。参数上,raw_result统一转成字符串再判断,避免工具返回非字符串类型时出错。这层包装放在调度循环里、追加到消息列表之前调用即可。另一个习惯是给每个工具调用打上时间戳和轮次编号,写进日志。Agent 出问题时,日志是你唯一的后悔药。我见过太多人只打印模型回复,不打印工具输入输出,排查时完全靠猜。把iteration、tool_name、tool_input、observation四样东西记全,九成的诡异问题都能定位。最后说一个我自己的教训:不要一上来就注册十几个工具。工具越多,模型选错的概率越高,调试难度指数上升。先用两三个工具把闭环跑稳,确认解析、回填、退出条件都没问题,再按业务需要逐个加。每加一个工具,都单独测一轮“只给它这一个工具”的场景,确认模型能正确调用,再放进工具池。这个笨办法帮我省下了大量排查时间。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源是字节跳动开源的Agent实践手册配套可运行源码包,面向企业内部业务与技术团队、AI应用开发者及希望将智能体技术落地到实际业务中的技术人员。手册系统总结了字节在飞书、抖音、教育等多条业务线落地Agent的方法、工具与规范,涵盖核心技术组件、开发流程、应用场景、运营优化、安全合规、风险应对、团队协作及全球化适配等内容,源码包则提供了可直接运行的配套示例,帮助读者从0到1理解企业级AI智能体的搭建思路。压缩包共3个文件,以html页面、inscode配置和gitignore忽略规则为主,整体约6KB,体量轻便,便于快速浏览与本地运行。目前已有824人学习下载,适合想了解Agent工程化落地路径、参考真实业务案例并动手实践的技术人员。1. 从一份可运行源码说起:Agent 手册到底解决了什么很多人第一次接触 Agent 开发,卡住的地方不是模型调用,而是“模型会说话,但不会干活”。你让它查天气,它编一个温度;你让它读文件,它给你一段伪代码。问题的根子在于:大模型本身只是一个文本生成器,它没有手也没有脚。所谓 Agent,就是给模型装上手脚的那套调度框架——把模型输出解析成工具调用,把工具执行结果塞回上下文,循环往复直到任务完成。这份“字节开源 Agent 手册”附带可运行源码,价值就在这里:它不是又一篇讲 ReAct 是什么的概念文,而是一套能跑起来的骨架。你拿到手,改几个配置就能接自己的模型、注册自己的工具、跑通一个完整的“思考—行动—观察”闭环。适合两类人:一是想从零理解 Agent 内部调度逻辑的开发者,二是有具体业务场景(比如自动查数据库、自动整理文件)想快速搭原型的工程师。下面我按自己复现这套东西的顺序,把关键环节拆开讲。2. 拆开 Agent 手册的骨架:调度循环与工具注册怎么落地2.1 先看清一个最小 Agent 的三个组成部分不管手册里封装了多少层,一个能跑的 Agent 核心就三块:模型客户端、工具注册表、调度循环。模型客户端负责发请求拿回复;工具注册表维护“工具名 → 函数 → 参数描述”的映射;调度循环负责把用户输入、历史消息、工具定义拼成 prompt,发给模型,解析回复,决定是继续调工具还是返回最终答案。我一般会先画一张数据流:用户输入 → 拼 prompt → 模型返回 → 解析出工具调用 → 执行工具 → 结果追加到消息列表 → 再次拼 prompt → …… → 模型返回纯文本 → 结束。这个循环的退出条件很关键,常见做法是判断模型回复里有没有工具调用标记,没有就认为它给出了最终答案。手册源码里通常用一个max_iterations兜底,防止模型陷入死循环。2.2 用 Python 跑通第一个工具调用闭环下面这段代码是我从手册骨架里抽出来的最小可运行版本,去掉了具体业务工具,只留一个计算器,方便你先把闭环跑通再往里加东西。import json import re # 1. 工具注册表:工具名 -> (函数, 描述, 参数schema) TOOLS = {} def register_tool(name, description, parameters): def decorator(func): TOOLS[name] = { "function": func, "description": description, "parameters": parameters, } return func return decorator @register_tool( name="calculator", description="执行四则运算,输入一个数学表达式字符串", parameters={"expression": "string, 例如 '3 * (4 + 5)'"}, ) def calculator(expression: str) -> str: # 注意:生产环境不要直接用 eval,这里仅为演示 try: result = eval(expression, {"__builtins__": {}}, {}) return str(result) except Exception as e: return f"计算失败: {e}" # 2. 把工具注册表转成模型能看懂的描述文本 def build_tool_prompt(): lines = [] for name, meta in TOOLS.items(): lines.append(f"- {name}: {meta['description']} 参数: {meta['parameters']}") return "\n".join(lines) # 3. 模拟模型返回(真实场景替换为 API 调用) def fake_llm(messages): last = messages[-1]["content"] if "计算" in last and "Action:" not in last: return "我需要用计算器。\nAction: calculator\nAction Input: 12 * 8 + 5" return "最终答案:计算结果是 101" # 4. 调度循环 def run_agent(user_input, max_iterations=5): messages = [{"role": "user", "content": user_input}] for i in range(max_iterations): prompt = ( f"你可以使用以下工具:\n{build_tool_prompt()}\n" f"请用 Action/Action Input 格式调用工具,或直接给出最终答案。\n" f"对话历史:\n{json.dumps(messages, ensure_ascii=False)}" ) reply = fake_llm([{"role": "user", "content": prompt}]) print(f"[第{i+1}轮模型回复] {reply}") action_match = re.search(r"Action:\s*(\w+)", reply) input_match = re.search(r"Action Input:\s*(.+)", reply) if action_match and input_match: tool_name = action_match.group(1).strip() tool_input = input_match.group(1).strip() if tool_name in TOOLS: observation = TOOLS[tool_name]["function"](tool_input) messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"Observation: {observation}"}) else: messages.append({"role": "user", "content": f"Observation: 未知工具 {tool_name}"}) else: return reply return "达到最大迭代次数,任务未完成" if __name__ == "__main__": print(run_agent("帮我算一下 12 乘以 8 再加 5 等于多少"))这段代码的逻辑说明:register_tool装饰器把函数和它的元信息塞进全局字典,这是手册里最常见的工具注册方式,好处是新增工具不用改调度循环。build_tool_prompt把工具描述拼成文本,真实场景里这部分会作为 system prompt 的一部分发给模型。调度循环里用正则解析Action和Action Input,这是 ReAct 风格的经典解析方式,简单但够用。参数说明:max_iterations建议设 5 到 10,太小复杂任务跑不完,太大模型抽风时会浪费 token。eval那里务必换成安全的表达式解析库,我这里只是为了让你一眼看懂工具函数长什么样。跑通之后你会看到两轮循环:第一轮模型决定调计算器,第二轮拿到结果后给出最终答案。2.3 工具描述写得好不好,直接决定模型调不调得对这是血泪经验:很多人跑不通 Agent,不是循环写错了,是工具描述太潦草。模型只能通过你给的文字判断该不该调这个工具。描述里要写清楚三件事——这个工具干什么、什么时候用、参数长什么样。比如“查询数据库”这种描述就太模糊,模型不知道是查用户表还是订单表。改成“根据用户 ID 查询该用户的订单列表,输入为整数用户 ID”,命中率立刻不一样。手册源码里通常会给每个工具配一个 JSON Schema 风格的参数定义,即使你用的是纯文本 prompt,也建议把参数类型和示例写进去。我一般会在描述末尾加一句“如果参数缺失,请先向用户追问”,能减少模型瞎编参数的情况。3. 把手册接到真实模型和真实工具上:配置与改造清单3.1 替换模型客户端时要注意的三个接口差异手册里的模型调用层通常做了抽象,但不同厂商的接口在三个地方有差异:消息格式、工具调用返回结构、流式输出。消息格式上,有的要求role只能是 system/user/assistant,有的允许 tool 角色;工具调用返回结构上,有的把工具调用放在content里让你自己解析,有的直接返回结构化的tool_calls字段。我一般会写一个适配层,把不同厂商的返回统一成内部格式。下面是一个适配函数的骨架:def normalize_response(raw_response): """ 把不同厂商的模型返回统一成: {"type": "tool_call", "tool": "xxx", "input": "..."} 或 {"type": "final", "content": "..."} """ # 情况一:结构化 tool_calls 字段 if hasattr(raw_response, "tool_calls") and raw_response.tool_calls: call = raw_response.tool_calls[0] return { "type": "tool_call", "tool": call.function.name, "input": call.function.arguments, } # 情况二:纯文本里用正则抠 Action text = raw_response.content if hasattr(raw_response, "content") else str(raw_response) action_match = re.search(r"Action:\s*(\w+)", text) input_match = re.search(r"Action Input:\s*(.+)", text) if action_match and input_match: return { "type": "tool_call", "tool": action_match.group(1).strip(), "input": input_match.group(1).strip(), } return {"type": "final", "content": text}逻辑说明:这个函数把两种主流返回形态收敛成同一种内部事件,调度循环只需要处理tool_call和final两种类型。参数上,raw_response可能是对象也可能是字符串,所以做了hasattr判断。改造时把调度循环里的解析逻辑换成调用这个函数即可,循环本身不用动。3.2 注册一个真实工具:从文件读取到结果回填拿“读取本地文本文件并统计行数”举例,这是最不容易翻车的真实工具,适合用来验证整条链路。@register_tool( name="count_lines", description="读取指定路径的文本文件,返回文件总行数。输入为文件绝对路径字符串。", parameters={"path": "string, 例如 '/data/logs/app.txt'"}, ) def count_lines(path: str) -> str: import os if not os.path.isfile(path): return f"文件不存在: {path}" try: with open(path, "r", encoding="utf-8") as f: lines = f.readlines() return f"文件共 {len(lines)} 行" except Exception as e: return f"读取失败: {e}"逻辑说明:工具函数必须自己处理异常并返回字符串,因为调度循环只负责把返回值塞回上下文,不会帮你捕获异常。参数说明:路径建议要求绝对路径,相对路径在不同工作目录下行为不一致,是常见翻车点。注册完之后,在build_tool_prompt里会自动带上这个工具的描述,模型就能在需要时调用它。3.3 用配置文件管理模型参数和工具开关手册源码里一般会有一个配置文件,把模型名、温度、最大 token、启用的工具列表放进去。我建议至少把这三类参数外置:模型连接参数(base_url、api_key、model_name)、生成参数(temperature、max_tokens)、调度参数(max_iterations、工具白名单)。这样换环境不用改代码,也方便做 A/B 对比。一个简单的 YAML 配置示例:model: base_url: "https://your-endpoint/v1" model_name: "your-model" temperature: 0.2 max_tokens: 1024 agent: max_iterations: 8 enabled_tools: - calculator - count_lines温度建议设低一点,0.1 到 0.3 之间,Agent 任务需要稳定复现,高温会让模型在工具调用格式上自由发挥,解析失败率飙升。enabled_tools用来做工具白名单,工具多了之后模型容易选错,按场景裁剪比全量注册更可靠。4. 避坑与排查:Agent 跑不通时先看这五条4.1 模型一直不调工具,只给文字答案现象:循环第一轮模型就返回“最终答案”,根本没走工具调用。原因通常是工具描述没写清楚,或者 prompt 里没有明确要求“需要外部信息时必须调用工具”。解决:在 system prompt 里加一句“如果你不确定答案,必须调用工具获取信息,不要凭记忆回答”,同时检查工具描述是否包含具体使用场景。4.2 工具调用格式解析失败,循环空转现象:模型明明输出了工具名,但正则没匹配上,调度循环认为它给了最终答案。原因是不同模型输出的格式有细微差异,比如Action:后面多了空格、换行,或者用了中文冒号。解决:正则里用\s*容忍空白,同时把Action[::]写成兼容中英文冒号。更稳妥的做法是优先使用模型厂商的结构化工具调用字段,正则只作为兜底。4.3 工具执行结果太长,把上下文撑爆现象:调用一个查询工具返回了几千行数据,下一轮模型直接报超长错误。原因是工具函数没有对返回值做截断。解决:在工具函数里限制返回长度,比如只返回前 2000 字符,并加一句“结果已截断,如需完整数据请缩小查询范围”。调度循环里也可以加一个统一的返回值截断逻辑。4.4 多轮之后模型忘记原始任务现象:循环到第三四轮,模型开始答非所问。原因是消息列表越来越长,早期用户输入被淹没。解决:在每轮拼 prompt 时,把原始用户任务单独放在最前面并加粗标记,或者维护一个“任务摘要”字段,每轮都带上。手册源码里如果有消息裁剪逻辑,检查它是不是把最早的 user 消息裁掉了。4.5 工具函数抛异常导致整个循环崩溃现象:某个工具遇到异常输入直接抛出未捕获异常,程序退出。原因是工具函数没有做 try/except。解决:所有工具函数内部必须捕获异常并返回错误字符串,让模型看到错误信息后有机会自我修正。调度循环外层也建议包一层 try/except,记录日志后继续下一轮,而不是直接崩掉。5. 让 Agent 更稳的一个技巧:给工具调用加一层结果校验跑通基本闭环之后,你会发现最大的不稳定来源不是模型能力,而是模型“以为”工具执行成功了。比如它调了计算器,工具返回“计算失败: invalid syntax”,但模型下一轮仍然基于一个错误结果往下推理。我一般会在调度循环里加一层轻量校验:工具返回后,检查返回值里有没有“失败”“错误”“不存在”这类关键词,如果有,就在 Observation 前面加一句“工具执行未成功,请检查参数后重试”。这个技巧不需要改模型,也不需要复杂的框架,就是在回填 Observation 的地方多几行判断:ERROR_KEYWORDS = ["失败", "错误", "不存在", "异常", "invalid"] def wrap_observation(tool_name, raw_result): lowered = raw_result.lower() if any(kw in lowered for kw in ERROR_KEYWORDS): return ( f"工具 {tool_name} 执行未成功,返回信息:{raw_result}\n" f"请检查输入参数是否正确,或换一种方式重试。" ) return f"工具 {tool_name} 执行成功,返回:{raw_result}"逻辑说明:ERROR_KEYWORDS是可扩展的,你可以按自己工具的实际错误文案往里加。参数上,raw_result统一转成字符串再判断,避免工具返回非字符串类型时出错。这层包装放在调度循环里、追加到消息列表之前调用即可。另一个习惯是给每个工具调用打上时间戳和轮次编号,写进日志。Agent 出问题时,日志是你唯一的后悔药。我见过太多人只打印模型回复,不打印工具输入输出,排查时完全靠猜。把iteration、tool_name、tool_input、observation四样东西记全,九成的诡异问题都能定位。最后说一个我自己的教训:不要一上来就注册十几个工具。工具越多,模型选错的概率越高,调试难度指数上升。先用两三个工具把闭环跑稳,确认解析、回填、退出条件都没问题,再按业务需要逐个加。每加一个工具,都单独测一轮“只给它这一个工具”的场景,确认模型能正确调用,再放进工具池。这个笨办法帮我省下了大量排查时间。希望帮到你。本文还有配套的精品资源,点击获取