1. 项目概述与核心价值
1.1 AgentChat 到底是什么
先直接说结论:AgentChat 不是一个玩具项目,它本质上是一个“能干活”的对话系统。传统聊天机器人只能一问一答,你说一句它接一句,而 AgentChat 里跑的是一个智能体循环——模型不仅能理解你的话,还能自己决定调用哪些工具、按什么顺序调用、拿到结果后再继续推理,直到完成你交付的任务。
很多人会把 AgentChat 和普通 Chatbot 搞混。举个最直白的例子:普通 Chatbot 就像前台客服,你问“几点了”它回答“现在下午三点”;AgentChat 更像一个实习生,你告诉它“帮我整理一下这个文件夹里的报告,提取关键结论,列成清单发我邮箱”,它会拆解任务、遍历文件、调用解析工具、汇总结果、调用邮件接口——整个过程不需要你一步步指挥。
我最早接触这个概念是在做内部自动化工具时,团队需要一个能连接数据库、文件系统、API 服务的中枢节点,而不是再写一堆死板的 if-else 调度脚本。后来大模型能力上来了,这个中枢节点自然就演变成了 AgentChat。现在你可能也发现了,市面上各种 AI 助手、Copilot、自动化工作流,底层核心架构基本都是这么一套东西。所以从零搭一个简单的 AgentChat,不是为了写 demo 炫技,而是在理解现在所有 AI 应用的地基。
1.2 这个项目能解决什么问题
从实际应用角度来说,搭建 AgentChat 的首要价值是打破模型“只能聊天、不能做事”的局限。底层大模型无论多聪明,它的知识截止到训练时间,它也无法直接读取你本地的数据库、调用你公司的内部接口。AgentChat 就是那个“接线员”——把模型和外部世界连接起来。
其次,它帮你把复杂任务自动化。比如我举个例子,工作场景里经常要查各种数据写周报。传统方式是手动打开后台、拉数据、导表格、整理成文字。有了 AgentChat 之后,你可以让它“查一下本周各渠道的转化率、对比上周涨跌、挑出异常项、生成一份周报草稿”。系统会自动完成:调用数据查询工具 → 获取原始数据 → 代码解释器做统计分析 → 模型生成报告。这个过程只需要自然语言描述一遍。
再者,对于开发者来说,AgentChat 是一个极佳的框架学习样本。你理解了它的循环机制之后,不管以后是接触 LangChain、AutoGen 还是自己手写编排逻辑,核心都是同一套思想:模型推理 + 工具执行 + 结果反馈 + 再推理。这个思维模型一旦建立了,看任何 AI 应用框架都会很通透。
1.3 哪些人适合参考这篇文章
如果你是有一定 Python 基础、想上手 AI Agent 开发的开发者,这篇内容完全为你定制。不需要你提前精通大模型原理,你只要会写基本的 Python 代码、懂函数定义、能调用 API 就可以。我会把 AgentChat 的每个模块拆开讲清楚,包括设计思路、代码组织方式、工具注册机制、循环终止条件这些关键点。
如果你是产品经理、技术爱好者或者刚入行的数据工程师,这篇文章同样有价值。你不用亲手敲代码,但读完你能理解 AgentChat 的系统边界在哪里、为什么智能体会“卡住”、为什么工具设计这么重要、为什么有人会说“Agent 的上限取决于工具的质量”。这些认知能帮你在跟开发协作时沟通得更顺畅,也能让你对市面上各种 AI 产品的底层运作有更准确的判断。
2. 整体设计与思路拆解
2.1 核心概念:从“对话”到“Agent 循环”
要理解 AgentChat,必须先理解一个核心概念——Agent 循环(Agent Loop)。它跟传统对话系统的本质区别在于控制流的归属。
传统对话系统里,控制流由开发者预设好。用户说“查天气”,代码里写死了匹配“查天气”关键词就去调天气 API,然后返回结果。系统能干什么完全取决于开发者写了多少条分支逻辑,模型只是做关键词识别和话术拼接。
AgentChat 里,控制流交给模型自己决定。这就好比你把一个笼统的目标交给了一个有判断力的执行者,它能自己规划步骤、选择工具、评估结果,做不了的时候还能向你提问。
具体的循环流程是这样的:
- 接收用户输入(通常是自然语言描述的目标)
- 把输入和可用的工具清单一起发给大模型
- 模型分析任务,决定是否需要调用工具;如果需要,输出一个结构化的调用指令(包含工具名和参数)
- 系统解析这个指令,找到对应的工具函数,执行它,拿到结果
- 把工具返回的结果回传给模型
- 模型根据结果继续推理,要么再调用下一个工具,要么输出最终答案结束循环
这套循环正是 AgentChat 的灵魂。设计它的时候有三件事是必须想清楚的:怎么让模型知道有哪些工具可用、模型输出怎么解析成可执行指令、什么时候结束循环避免死循环。
2.2 技术选型为什么这么定
具体到实现层面,我的选型逻辑很直接:找个能力足够强的对话模型、写一段干净的工具注册代码、用最简单的 JSON 协议做模型与工具之间的消息交换。
模型选型上,我建议优先选支持 Function Calling / Tool Use 的模型,比如 OpenAI 的 gpt-4o 系列、Claude 的 tool use 能力、或者国产的 Qwen、GLM 也都支持类似功能。这些模型在训练阶段就针对“输出结构化工具调用指令”做过专门优化,输出格式稳定、参数填充准确,比自己写 Prompt 硬塞要可靠得多。我做过对比测试,用支持 Function Calling 的模型搭 Agent 比用通用模型 + 强 Prompt 约束的方式,工具调用准确率能高很多,关键是几乎不会出现输出格式错乱的问题。
工具注册机制上,我设计了一个非常轻量的装饰器方案。开发者只需要写一个普通的 Python 函数,然后在上面加一个@register_tool装饰器,填入函数描述、参数说明,系统自动把这个函数的信息加入工具清单。这个方案的优势在于新工具的接入成本几乎为零,团队里任何一个人都能在五分钟内新增一个 Agent 能力。
消息协议上,我选了 JSON。这不是什么新潮设计,但它是当前生态里兼容性最好的格式。OpenAI 的 function call 输出是 JSON,Anthropic 的 tool_use 是 JSON,各大模型厂商基本都对齐了 JSON Schema 描述工具。我们自己也用 JSON 做内部协议,这样不仅跟各模型兼容,将来接不同的模型后端时也不用改核心逻辑。
2.3 从零搭建的模块划分
把 AgentChat 拆开看,核心模块大致是这几个:
- 模型接入层:负责跟大模型 API 打交道,把对话历史、工具定义发给模型,拿到模型的回复。这是 Agent 的“大脑”入口。
- 工具管理层:维护一个工具注册表,记录每个工具的名字、描述、参数 Schema、对应的 Python 函数。同时负责根据模型的调用指令,找到函数并执行。
- 消息组装器:负责把对话历史、工具调用结果、系统提示词按模型要求的格式组装好。每次循环都要重新组装一次,因为对话上下文在增长。
- Agent 执行引擎:整个循环的调度中心。它有状态管理功能,记录当前是第几轮、已经调用过哪些工具、累计消费了多少 token,还要设定最大迭代次数防止死循环。
- 用户交互层:命令行交互界面(或者后续扩展成 Web 界面),负责接收用户输入、流式显示模型回复、把工具调用的过程可视化展示。
模块划分清晰之后,AgentChat 的可扩展性就体现出来了——你想加一个新工具,只碰工具管理层;你想换一个更便宜的模型,只碰模型接入层;你想让 Agent 在 Web 端跑,只重写用户交互层。每一层之间的耦合度都很低,这是我认为值得借鉴的设计思路。
3. 环境准备与核心依赖
3.1 Python 环境和依赖清单
动手之前先把环境准备好。我的推荐版本组合如下,这些都是经过验证的稳定搭配:
# Python 3.10 以上版本 python --version # 安装核心依赖 pip install openai # 如果你选 Anthropic 作为模型后端 pip install anthropic # 或者用 Qwen 的 SDK pip install dashscope这个项目其实对第三方框架的依赖非常少,我不建议大家一开始就上手 LangChain 之类的全家桶。原因在后面细说。
3.2 为什么不用现成的 Agent 框架
你可能好奇:市面上有 LangChain、AutoGen、CrewAI 这么多现成框架,为什么还要从零搭?
我自己用过这些框架,坦白说它们很强大,但也存在问题:抽象层级太高,出了问题很难排查。你调用一个高层 API,底层做了很多事情,一旦中间某个环节出错,你得一层层扒源码才能定位。对于学习阶段来说,这种黑盒体验非常糟糕。
从零搭建的 AgentChat 代码量其实很小,核心引擎部分大概两百行左右。你能看到每一步在做什么:消息怎么组装、工具怎么被调用、结果怎么回传。这种透明度带来的掌控感,是框架给不了的。
更重要的是,当你理解了底层原理之后,再回头用 LangChain 这类框架,你会清晰地知道它在每一层替你做了什么。这时候框架反而是助力而不是黑盒。所以我一直建议:先用原始方式搭一遍,再考虑框架。这跟学编程先学数据结构再学框架是一个道理。
4. 核心模块的代码逻辑解析
4.1 工具注册机制的设计与实现
工具注册是 AgentChat 最基础的能力。我把每个工具看成一个独立函数,并用一个全局注册表统一管理。这里用 Python 装饰器实现,非常简洁:
# tool_registry.py from typing import Callable, Dict, Any import inspect import json TOOL_REGISTRY: Dict[str, Dict[str, Any]] = {} def register_tool(name: str, description: str, parameters: dict): """ 工具注册装饰器。 name: 工具的唯一名称 description: 工具功能描述,这是给模型看的,越详细越好 parameters: JSON Schema 格式的参数定义 """ def decorator(func: Callable) -> Callable: TOOL_REGISTRY[name] = { "name": name, "description": description, "parameters": parameters, "func": func, } return func return decorator def get_tool_schemas() -> list: """返回所有工具的 JSON Schema 描述,用于发给模型""" schemas = [] for tool in TOOL_REGISTRY.values(): schemas.append({ "type": "function", "function": { "name": tool["name"], "description": tool["description"], "parameters": tool["parameters"], }, }) return schemas def execute_tool(tool_name: str, arguments: dict) -> Any: """根据工具名和参数执行对应的 Python 函数""" if tool_name not in TOOL_REGISTRY: raise ValueError(f"Unknown tool: {tool_name}") tool = TOOL_REGISTRY[tool_name] return tool["func"](**arguments)这段代码的核心价值在于:把“工具是什么”和“怎么执行”彻底分开。工具的定义(name、description、parameters)是给模型看的,目的是让模型理解“有什么工具、什么时候用、传什么参数”;而工具的执行(func)是给 Python 解释器看的,目的是真正干活。
装饰器方案还有一个额外的好处:工具列表是自动收集的。你新增一个工具时,不用去改任何注册中心的代码,get_tool_schemas()会自动把新工具纳入清单。这在框架设计里叫“开闭原则”——对扩展开放,对修改关闭。
4.2 具体工具的编写示例
光有注册机制还不够,得有几个实际工具演示。这里写三个最常见的:获取当前时间、做数学计算、访问某个模拟的数据接口。
# tools.py import datetime import json from tool_registry import register_tool @register_tool( name="get_current_time", description="获取当前日期和时间,返回格式为 YYYY-MM-DD HH:MM:SS", parameters={ "type": "object", "properties": {}, } ) def get_current_time(): return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @register_tool( name="calculator", description="执行数学计算,支持加减乘除、幂运算等,输入一个字符串表达式", parameters={ "type": "object", "properties": { "expression": { "type": "string", "description": "合法的数学表达式,例如 '3 * (4 + 5)'", } }, "required": ["expression"], } ) def calculator(expression: str): # 用 eval 需要非常小心,这里是演示,生产环境请用 ast 模块做安全校验 return eval(expression) @register_tool( name="query_user_info", description="根据用户 ID 查询用户基本信息,返回姓名、邮箱、注册日期", parameters={ "type": "object", "properties": { "user_id": { "type": "string", "description": "用户 ID,例如 'U12345'", } }, "required": ["user_id"], } ) def query_user_info(user_id: str): # 模拟数据,实际项目里这里可以连数据库 mock_db = { "U12345": {"name": "张三", "email": "zhangsan@example.com", "registered": "2023-06-15"}, "U67890": {"name": "李四", "email": "lisi@example.com", "registered": "2024-01-08"}, } return mock_db.get(user_id, {"error": "user not found"})注意一下工具描述的重要性。我在代码里对每一项 description 都写得比较精确,这不是随意写的。模型是通过 description 来理解工具用途的,如果你的描述太模糊,模型就可能在不合适的场景下调用工具。比如calculator的描述里我明确写了“输入一个字符串表达式”,这样模型就会知道参数格式。我一开始写的时候 description 只写了“计算器”,结果模型经常把参数以 JSON 对象形式传过来,解析时各种报错。描述写的越清楚,后面的麻烦越少。
4.3 模型接入层:构建消息并调用 Function Calling
模型接入层是 AgentChat 跟大模型通信的桥梁。这里以 OpenAI 兼容接口为例:
# model_client.py import json from openai import OpenAI from tool_registry import get_tool_schemas client = OpenAI( api_key="your-api-key", base_url="your-base-url", # 如果使用兼容接口的第三方服务 ) SYSTEM_PROMPT = """你是一个智能助手,可以通过调用工具来帮助用户完成任务。 当用户的需求需要工具协助时,请调用合适的工具。如果无法通过工具完成, 请如实告知用户。请注意:工具执行结果返回后,请基于结果继续回答用户。""" def chat_with_tools(messages: list): """发送对话消息和工具定义给模型,返回模型回复""" response = client.chat.completions.create( model="gpt-4o", # 换成你实际使用的模型 messages=messages, tools=get_tool_schemas(), tool_choice="auto", # 让模型自己决定是否调用工具 ) return response.choices[0].message这里的关键参数是tools=get_tool_schemas()和tool_choice="auto"。前者把当前所有可用工具告诉模型,后者让模型自行判断是否需要调用工具、调用哪个。如果模型认为不需要调用工具,它就直接返回普通文本回复;如果需要调用,它的回复里会带tool_calls字段,里面是结构化的工具名和参数 JSON。
还有SYSTEM_PROMPT也很重要。虽然 Function Calling 已经很智能,但一个简洁清晰的系统提示词能让模型的工具使用更规范。我在实践里总结的经验是:不要写太长的角色设定,把核心规则交代清楚即可——“需要时调用工具、调用后根据结果继续回答”。
4.4 主循环引擎:组装、调用、回传、迭代
现在到了整个 AgentChat 的心脏——主循环引擎。这段代码负责把模型、工具、消息历史串起来:
# agent.py import json from model_client import chat_with_tools, SYSTEM_PROMPT from tool_registry import execute_tool class AgentChat: def __init__(self, max_iterations: int = 5): self.messages = [{"role": "system", "content": SYSTEM_PROMPT}] self.max_iterations = max_iterations def run(self, user_input: str): # 把用户输入加入消息历史 self.messages.append({"role": "user", "content": user_input}) for i in range(self.max_iterations): print(f"\n--- 第 {i + 1} 轮迭代 ---") # 1. 调用模型 response_message = chat_with_tools(self.messages) print(f"模型回复: {response_message}") # 2. 把模型的回复加入历史,这是必须的 self.messages.append(response_message) # 3. 判断模型是否要调用工具 if not response_message.tool_calls: print("模型判定无需调用工具,返回最终答案。") return response_message.content # 4. 逐个处理工具调用 for tool_call in response_message.tool_calls: tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments or "{}") print(f"调用工具: {tool_name}, 参数: {tool_args}") # 5. 执行工具并获取结果 result = execute_tool(tool_name, tool_args) # 6. 把工具结果以 tool 角色消息加入历史 self.messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) # 7. 进入下一轮迭代,让模型基于工具结果继续推理 # 8. 超过最大迭代次数,强制终止 print(f"达到最大迭代次数 {self.max_iterations},自动终止。") return "任务未能完成,已达到最大迭代限制。"这段主循环里每一步都有着不可替代的作用,缺一步整个循环就转不起来。我拆开解释几个关键细节:
第一,模型的 assistant 回复必须原样加入消息历史。这不仅仅是记录对话,更重要的是模型下一轮推理时能看到自己上一轮的选择和判断。如果你不加入,模型会丢失上下文,就会陷入“失忆”状态。
第二,工具执行结果也必须以 tool 角色的消息加入历史,而且tool_call_id必须和模型的工具调用 ID 对应。这是 Function Calling 协议的要求——模型要看得到“我调用了某个工具、工具返回了什么”,才能继续判断下一步动作。ID 对不上或者角色搞错,模型就不知道这段结果跟哪个调用关联。
第三,循环的终止条件有两个:一是模型不再输出 tool_calls,直接给出最终答案;二是达到我们设置的最大迭代次数上限。第二个条件极其重要,我刚写 AgentChat 的时候没加这个限制,结果有一次模型死循环调了几十次工具,token 消耗非常惊人。加了限制之后,最坏情况也就是任务失败,但不会失控。
4.5 命令行交互界面
最后加一个简单的命令行入口,让 AgentChat 能真正跑起来:
# main.py from agent import AgentChat def main(): agent = AgentChat(max_iterations=5) print("AgentChat 已启动,输入你的需求,输入 /quit 退出。") while True: user_input = input("\n你: ") if user_input.strip().lower() in ("/quit", "/exit", "quit", "exit"): print("再见!") break final_answer = agent.run(user_input) print(f"\nAgent: {final_answer}") if __name__ == "__main__": main()这个交互层暂时只有最基本的功能,但它体现了 AgentChat 的一个特性:对话历史在累积。agent.run()内部把每轮的用户输入和 agent 回复都保存在self.messages里,所以 Agent 是有记忆的。你可以先问“查询 U12345 的用户信息”,再继续问“这个用户的注册日期是什么”,它可以基于上文回答。这种记忆能力是实现多轮任务的基础。
5. 实操运行与效果验证
5.1 启动项目的完整流程
代码写完以后,实际的启动流程非常直接。把项目文件按下面的结构放好:
agentchat/ ├── main.py # 入口文件 ├── agent.py # Agent 主循环 ├── model_client.py # 模型接入层 ├── tool_registry.py # 工具注册机制 └── tools.py # 具体工具定义然后运行:
python main.py看到AgentChat 已启动的提示,就说明系统已经就绪了。这个过程中如果报错,绝大多数情况是依赖没装全,或者 API Key 配置有问题。建议先把model_client.py里的 API 配置项检查一遍。
5.2 三个典型运行场景演示
我实际运行了一下,挑三个典型场景分享。第一个场景是单工具调用:
你: 现在几点了? Agent: 2025-01-15 14:32:08运行过程里你能看到模型判断出需要调用get_current_time工具,执行后拿到时间,再组织语言回复。这个场景虽然简单,但它完整走了一遍“模型决策 → 工具执行 → 结果返回 → 模型回答”的链路。
第二个场景是多工具协作。比如说:
你: 帮我查一下 U67890 这个用户的情况,然后算一下他注册了多少天。这个过程模型会先调用query_user_info拿到注册日期,然后调用calculator算出时间差,最后汇总两个工具的结果给你一个完整的回答。这种场景最能体现 Agent 的价值——多个工具串起来完成一个复合任务,你不需要分两次提问。
第三个场景是不需要调用工具的普通对话:
你: 你好,介绍一下你自己。 Agent: 我是一个智能助手,可以通过调用工具来帮助你查询信息、执行计算等任务。这种情况下模型直接返回文本,不会强行调用工具。这得益于tool_choice="auto"的设置,模型有判断力去决定“这个需求不需要工具”。
5.3 运行过程中观察到的效果与边界
运行一段时间之后,我对 AgentChat 的实际能力和边界都有了更直观的认识。能力的部分不用多说,它确实能做普通聊天机器人做不到的事。我需要提醒的是边界,尤其是模型的判断能力依赖模型本身的智商。
模型如果不够强,它可能在“该调用工具”的时候不调用,或者在“不该调用”的时候瞎调用。我就遇到过 gpt-3.5 时代的模型,它面对“今天是几号”这种明确需要工具的问题,居然靠训练数据里的记忆瞎猜了一个日期。换更强的新模型之后,这种情况就很少出现。所以如果你想认真用 AgentChat 做事情,建议直接用当前最强的模型,虽然贵一点,但稳定性和准确率对得起差价。
另一个观察是工具数量对模型选择的影响。我一开始只注册了两个工具,模型选择非常准确;后来一口气注册了十几个工具,模型偶尔会出现找错工具的情况。这说明工具描述要写得足够差异化,尤其是功能相近的工具,描述里的区分度至关重要。
6. 常见问题与排查技巧实录
6.1 模型不调用工具或调用格式错误
这是新手搭建 AgentChat 时遇到频率最高的一类问题。模型就是不调用工具,明明工具列表已经传过去了,它却直接生成文本答案。或者调用了,但返回的 JSON 参数格式乱七八糟,函数执行直接报错。
排查思路要从三个层面走。第一,确认模型版本是否支持 Function Calling,很多模型 API 需要显式启用这个能力,旧版本模型根本不支持结构化工具调用;第二,确认工具描述是否清晰,把 description 写得更具体些,明确说清楚“什么时候应该用这个工具”,这一点非常管用;第三,检查消息格式是否符合 API 要求,如果消息里混入了不符合格式的历史记录,模型可能直接放弃工具调用。
我自己的一个排查经验是:把发给模型的原始请求打印出来,人工看一遍。很多问题一眼就能发现——要么是函数描述参数不完整,要么是历史消息角色混乱。把请求内容转成可读的 JSON 打印出来调试,是定位这类问题的最高效手段。
6.2 工具调用结果丢失或上下文错乱
另一个常见问题是:工具执行成功了,结果也拿到了,但模型下一轮推理时似乎“忘了”这个结果,回答得驴唇不对马嘴。
这个问题九成出在消息历史的组装上。工具调用的结果必须以role: "tool"的角色进入消息历史,并携带正确的tool_call_id。如果你的代码里忘了这一步,或者把工具结果错误地以role: "user"发回去,模型就无法正确关联“第一次调用 → 对应结果”的对应关系,推理自然乱套。
也有一种情况是tool_call_id对不上。模型的 tool_calls 里每个调用都有唯一的 id,你得把执行结果关联到正确的那个 id 上。如果同一个回复里调用了多个工具,每个结果都要用自己的 id 单独回传,不能混淆。这个细节在检查代码时经常被忽略,但一旦错了,整个上下文就全乱了。
6.3 死循环与 Token 爆炸问题
AgentChat 跑着跑着停不下来,一轮又一轮地调用工具,Token 消耗蹭蹭往上涨。这是最让人头疼的问题,也是我一开始差点放弃 AgentChat 的点。
解决方案主要有两板斧。第一板斧是设置max_iterations上限,从根上掐断死循环的可能。我的默认值是 5,简单任务三轮内能解决,复杂任务五轮已经足够。第二板斧是观察循环轨迹。在每一轮迭代打印模型回复和工具调用信息,你能直接看到它在哪个环节反复横跳。比如我遇到过模型反复调用同一个工具、参数还一模一样的情况,那就是典型的“陷入循环陷阱”,需要调整工具描述或者增加系统提示词的引导。
还有一个进阶技巧是引入 Token 预算。在 Agent 引擎里累计每轮消耗的 token 数,超过预算就强制停止。这个机制在做钱包保护时特别有用,毕竟模型调用的费用是实打实的。
6.4 工具执行报错处理与提示词优化
工具函数本身出错是不可避免的,尤其是跟外部系统交互的时候。比如你查数据库,库连不上;调 API,接口报 500。这个问题的处理方案很简单:不要把错误直接抛出去中断整个 Agent 循环,而是把错误信息当作工具结果返回给模型。
def execute_tool(tool_name: str, arguments: dict) -> Any: if tool_name not in TOOL_REGISTRY: return {"error": f"Unknown tool: {tool_name}"} try: tool = TOOL_REGISTRY[tool_name] return tool["func"](**arguments) except Exception as e: return {"error": str(e)}这样做的意义在于:模型看到错误信息后,可以自己判断是换个参数重试、还是换一种方式解决问题、或者告诉用户任务无法完成。这比直接把异常抛出来让整个程序崩溃要健壮得多。AgentChat 的精髓就在这里——它允许“出错”,但要求“出错后可恢复”。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查方案 |
|---|---|---|
| 模型从不调用工具 | 模型不支持 Function Calling / 工具描述不清晰 | 更换支持工具调用的新模型;重写工具 description,明确使用场景 |
| 工具参数解析报错 | 模型返回的 JSON 格式不规范 / 参数 Schema 定义错误 | 在解析处加 try-except 并打印原始参数;检查 JSON Schema 的 required 字段 |
| 模型忽略工具执行结果 | tool 角色消息未正确加入历史 / tool_call_id 不匹配 | 确保每条 tool 消息都带对应 id;检查消息顺序是否错乱 |
| Agent 陷入死循环 | 模型反复调用同一工具 / 任务目标模糊 | 设置 max_iterations;在提示词中要求“如果工具结果多次相同则停止” |
| Token 消耗过高 | 迭代次数太多 / 工具返回结果太长 | 限制 max_iterations;工具结果做截断处理 |
| 工具函数报错导致程序崩溃 | 未捕获工具内部异常 | 在 execute_tool 中统一捕获异常,把错误信息作为结果返回 |
7. 进阶扩展建议
7.1 增加流式输出提升体验
现在这个版本的 AgentChat 回答是“一次性打印”的,交互体验上比较生硬。你可以改成流式输出——模型一个字一个字地往外蹦,工具调用过程也能实时展示。这对于面向用户的场景是必要的,因为人脑处理信息需要时间,流式输出能让用户感觉系统“在思考”,而不是卡住了。
具体的实现方式是把模型调用的stream=True打开,然后对返回的流式 chunk 做逐段处理。工具调用的部分逻辑会复杂一些,因为流式输出的 tool_calls 可能被拆成多个片段,需要手动拼接。这部分建议单独写一个流式消息组装器。
7.2 接入更多工具类型
当前的工具都是纯 Python 函数,你可以扩展出更丰富的能力。比如:
- 访问数据库(MySQL、PostgreSQL、MongoDB)
- 调用外部 HTTP API(直接在工具函数里发请求)
- 操作文件系统(读文件、写文件、遍历目录)
- 执行代码(在沙箱环境里跑一段 Python 脚本)
- 调用向量数据库做知识库检索(这是 RAG 场景的基础)
每加一种工具,都要遵循同一个步骤:写函数 → 加装饰器 → 定义参数 Schema。AgentChat 的扩展模式非常统一,这也是它易用的原因。
7.3 从命令行到真实产品
如果要把 AgentChat 做成一个真正可用的产品,还需要补上这些能力:
- 持久化存储:对话历史存到数据库,重启不丢。
- 多用户支持:每个用户有独立的会话隔离。
- 权限管理:哪些工具对哪些用户开放,防止越权调用。
- 可观测性:记录每一次工具调用的耗时、参数、结果,方便排查。
- 异步化:用 FastAPI 包一层,支持 Web 端长连接。
这一步是从“个人玩具”走向“可用的服务”的分水岭。架构上主循环不需要变,主要是在外层增加管理和调度能力。
8. 实操总结与个人经验分享
做了这么多轮实验,我最大的体会有两个。第一个是:Agent 的上限由工具决定,而不是由模型决定。模型再聪明,如果你的工具只覆盖了一小部分场景,Agent 能做的事情就相当有限。反过来,工具设计得丰富、描述得清晰,Agent 的可用性会成倍提升。所以我后续开发 AgentChat,大部分精力都花在“如何把更多能力接入系统”和“如何让工具描述更准确”上,而不是反复调模型参数。
第二个体会是:AgentChat 调试的关键在于让过程可见。每一轮迭代,模型说了什么、调了哪个工具、工具返回了啥,这些信息一定要打印出来或者记录下来。Agent 的“黑盒感”比普通程序强得多,如果过程不透明,出了问题只能猜。加上日志之后,绝大多数问题都能通过观察运行轨迹找到原因。
最后再分享一个小技巧:给 Agent 写系统提示词的时候,可以加一句“如果你认为任务无法通过现有工具完成,请直接告知用户,不要强行调用工具”。这句话能省掉很多不必要的 token 消耗,也能避免模型为了“迎合用户”而去做一些不合理的操作。从零开始搭一个 AgentChat 并不难,但让它变得可靠、可控、可扩展,需要你在实际使用中反复打磨。