从ChatBot到AI Agent:Function Calling与记忆系统实战指南 1. 先搞清楚ChatBot和Agent的差距到底在哪里1.1 两者本质差异你是在聊天还是在解决问题很多刚接触这个领域的人把ChatBot和Agent混为一谈这其实是个很大的误区。ChatBot的本质是一个语言生成器。你给它一段输入它根据训练时学到的模式生成一段文字返回给你。它没有真正的目标概念没有主动行动能力没有状态记忆。你问它帮我订一张明天去上海的机票它最可能的反应是非常清楚地把订票步骤列出来告诉你应该打开哪个App、点哪个按钮然后就没有然后了。它不会真的去帮你订票因为它压根没有执行这个概念它所有的能力都停留在生成建议的文本这个层面。Agent的本质是一个任务执行体。它接收一个目标然后把目标拆解成若干步骤按顺序执行这些步骤过程中可能需要调用外部工具搜索引擎、数据库、API接口等需要根据中间结果调整后续动作最后把任务结果返回给你。同样是帮我订票这个需求Agent会自己去查航班、对比价格、调用订票接口、完成支付然后告诉你已经订好了这是你的电子机票信息。我用一个生活化的类比来帮助你理解ChatBot像一本非常聪明的说明书你问什么它都能回答得很漂亮但回答完就完事了Agent更像一个有经验的助理你交代一件事他会自己琢磨怎么干、联系谁、按什么顺序做最后把结果交给你。一个是告诉你怎么做另一个是直接帮你做这就是本质区别。1.2 技术演进路线从单轮对话到自主决策理解技术演进能帮你看清Agent出现的必然性也能让你在面对各种新名词时不慌。第一代单轮问答。就是早期的对话系统你问我答答完结束。没有上下文没有记忆无法理解隐含意图。这类系统的代表就是早期的问答机器人。第二代多轮对话。引入了对话历史和上下文管理模型能记住你上一轮说了什么能进行多轮交互。ChatGPT这类产品就是这个阶段的典型代表它强在语言理解和生成但依然是对话完即结束不会主动去执行任何外部操作。第三代工具增强。模型开始学会调用外部工具比如搜索引擎、计算器、代码解释器等。这一阶段模型已经突破了纯文本的限制可以通过插件或函数调用来获取实时信息、执行计算。第四代自主智能体。也就是我们现在讨论的Agent。在工具能力的基础上进一步引入了目标拆解、任务规划、状态管理、自我反思、多步骤执行等能力。一个Agent可以把一个模糊的目标逐步细化自主推进遇到问题能够根据反馈调整策略。这种演进逻辑其实很像人类的成长过程从只会回答问题到学会使用工具再到学会为自己设定小目标并一步步完成。理解了这条线你就抓住了Agent设计的核心脉络后面学什么都会顺很多。1.3 Agent系统的三大核心模块虽然市面上Agent框架五花八门但拆开来看核心模块就三个感知模块负责接收和理解输入。包括识别用户意图、提取关键信息、理解当前环境状态。在代码里这就是解析用户请求的部分。决策模块这是Agent的大脑负责根据目标制定计划、选择行动策略。在代码里通常会看到循环加推理的结构当前要做什么、做完之后下一步怎么走。行动模块负责实际执行。它通过调用工具、访问API、读写数据等方式改变环境状态然后把执行结果反馈回决策模块。整个Agent系统跑起来的逻辑可以理解为感知到目标 → 制定计划 → 执行动作 → 观察结果 → 根据结果调整计划 → 继续执行 → 输出最终答案。这套感知-决策-行动-观察的循环机制就是Agent自动工作的核心引擎。后面我们做项目时你会发现代码写来写去都是在实现这个循环。提示很多教程一上来就讲各种框架的API用法我反而不建议新手这么学。先把感知、决策、行动这三个模块在脑子里立起来再看任何框架都会觉得原来是这么回事。2. 从零开始的实战先做出一个最小可用的ChatBot2.1 环境准备与模型选型思路工欲善其事必先利其器。我们先说环境再说选型。基础环境方面我默认你有Python 3.9以上的版本并且会使用pip安装依赖包。没有也没关系装一个Anaconda把Python环境一起解决了。整个系列要用到的核心依赖其实很少一个OpenAI SDK一个可以处理JSON的Python内置库再加上Flask或者FastAPI这类轻量框架做服务封装。就这些不需要重型框架。模型选型是很多新手最容易纠结的地方。我的建议是初期不要追求最强模型而要追求好调试、成本低。你可以选择国内各大厂商提供的API也可以使用开源模型本地部署关键是先把整个工程链路跑通。链路通了以后换模型只是换一个API配置的事成本极低。我在实际开发中习惯把模型调用部分封装成一个独立的模块。不要到处直接调API而是统一走一个接口。之后换模型、做缓存、加日志都方便这也是专业开发里很常用的做法。2.2 第一版代码五分钟搭一个带上下文的ChatBot我们用最简单的方式先做一个带上下文记忆的对话机器人。核心思路是把历史消息拼成消息列表每次请求时一起发送给模型。from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://your-model-endpoint ) def chat(): messages [ {role: system, content: 你是一个乐于助人的助手。} ] print(对话开始输入 exit 退出。) while True: user_input input(\n你: ) if user_input.lower() exit: break messages.append({role: user, content: user_input}) resp client.chat.completions.create( modelyour-model-name, messagesmessages, temperature0.7 ) assistant_msg resp.choices[0].message.content messages.append({role: assistant, content: assistant_msg}) print(f\n助手: {assistant_msg}) if __name__ __main__: chat()这个代码看着简单但已经包含了多轮对话的核心机制消息列表的维护。什么时候该加system消息什么时候追加user消息什么时候追加assistant消息逻辑都在里面了。你也许觉得这有什么难的对确实不难。但它是一个非常重要的基础因为Agent的消息管理复杂度会是这里的很多倍先把最简单的结构刻在脑子里。运行起来以后你就能感受到它记住你前面说过的话了。但这种记忆有个致命缺陷——它的记忆完全依赖于把所有历史请求都塞进上下文一旦对话历史变长消耗的Token会急剧膨胀响应速度也会变慢。这个问题在你做Agent的时候会非常突出后面我们会专门讲怎么解决。2.3 Token上下文限制问题第一个必踩的坑我实话说第一次把对话拉到十几轮就发现响应开始变慢而且成本肉眼可见地涨。原因很简单每轮请求都要携带全部历史消息Token数量按轮次线性增长。如果你做的是个人小工具这个问题可能还不致命。但如果你要做正式产品或服务这个问题必须处理通常有三种方案第一种滑动窗口截断只保留最近的N轮对话更早的直接丢弃。实现成本最低但会牺牲早期信息。第二种摘要压缩每次对话达到一定长度时调用模型把历史内容总结成一段摘要后续请求只携带摘要。信息保留率更好但实现稍复杂而且摘要过程本身也在消耗Token。第三种外部向量数据库存储把历史消息向量化后存入数据库每次请求时检索相关片段拼入上下文。这是目前主流方案也是后面做Agent记忆系统的基础。这个坑我建议你从一开始就意识到否则后面添加了工具调用后消息格式会变得更加复杂上下文长度问题会更快暴露出来。方向对了后期回头改的成本会小很多。3. Agent的核心魔法Function Calling与工具调用3.1 Function Calling到底是什么很多同学一听到Function Calling就觉得高深其实它解决的是一个非常朴素的问题大模型虽然聪明但它活在虚拟的文字世界里碰不到现实的数据和系统。它不知道此刻的天气不能实时搜索不能执行代码不能读写你的数据库。Function Calling给了它一双手。具体来说Function Calling是这样一件事你预先定义好一些函数告诉模型你有这些工具可以用每个工具长这样参数是什么。当用户提出请求时模型会判断这个问题需要调用某个工具然后输出一个结构化的调用请求。你的程序接收到这个请求之后去真真切切地执行对应的函数把执行结果拿回来再交还给模型让模型基于真实结果生成最终回答。我打个比方模型像一个指挥官函数是它手下的士兵。指挥官不会自己跑腿但知道每个士兵能干什么、需要什么指令。你说给我查查明天的天气指挥官判断这需要侦察兵出马于是下达指令侦察兵去查看明天的天气。侦察兵把消息带回来指挥官根据情报写出一份报告给你。3.2 工具定义的正确姿势Schema该怎么写在代码层面Function Calling是通过传入函数描述列表来实现的。每个函数描述是一个JSON Schema结构包含函数名、描述、参数结构。这里有一个关键描述一定要写清楚。模型是靠你的描述来判断什么时候该调用这个函数的描述写得模糊模型就经常犯糊涂。我们来看一个实际的工具定义例子以一个查询天气的工具为例tools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气信息。当用户询问天气、温度、降水概率等气象信息时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 } }, required: [city] } } } ]这段Schema里有两个细节值得注意。一个是description字段不要只写查询天气而是写清楚适用场景。模型本质上是在做语义匹配你的描述越接近用户的真实表达方式模型的判断越准确。另一个是参数名和描述参数名最好直接用拼音或英文的全名描述里告诉模型参数的具体含义和取值范围这样模型能更精准地填充参数。3.3 工具调用完整流程发起→执行→反馈工具调用的完整流程比很多人想象的多一个关键步骤当模型返回工具调用请求之后你不能直接把请求结果当答案返回给用户而是需要把函数执行的真实结果回传给模型让模型基于这个真实结果再生成一次回答。完整的调用循环是这样的第一步把用户的提问、历史消息、工具定义一起发给模型。第二步模型返回两种可能之一如果它认为不需要调用工具直接返回文本答案如果它认为需要调用工具返回一个结构化的工具调用指令包含函数名和参数。第三步你的代码检查响应内容。如果检测到工具调用请求就根据函数名在代码里找到对应的真实函数用模型给出的参数去执行它得到执行结果。第四步把函数执行结果作为一个新的消息追加到消息列表里再次发送给模型。第五步模型收到真实结果后结合原始问题和数据生成最终的自然语言回复给用户。我再强调一遍第五步是新手最容易被忽略的。我在刚开始做的时候就直接把工具结果抛给用户了结果是用户看到一大段未加工的JSON数据体验很差。正确的做法一定是让模型看着数据说话这样回复既准确又自然。3.4 一个完整的多工具Agent雏形这里我们做一个稍微完整点的示例同时提供天气查询、时间查询、计算器三个工具的Agent雏形。import json from datetime import datetime from openai import OpenAI client OpenAI(api_keyyour-api-key, base_urlhttps://your-model-endpoint) def get_weather(city: str) - str: # 实际项目接入真实天气API这里演示返回模拟数据 weather_map { 北京: 晴25℃, 上海: 多云28℃, 广州: 小雨30℃ } return weather_map.get(city, f{city}暂无天气数据) def get_current_time() - str: return datetime.now().strftime(%Y-%m-%d %H:%M:%S) def calculator(expression: str) - str: try: # 注意生产环境禁止直接eval用户输入这里仅作演示 result eval(expression) return str(result) except Exception as e: return f计算错误: {e} tools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气。当用户询问天气情况时使用。, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } }, { type: function, function: { name: get_current_time, description: 获取当前日期和时间。当用户询问时间、几点、日期时使用。, parameters: {type: object, properties: {}} } }, { type: function, function: { name: calculator, description: 执行数学计算。当用户请求计算加减乘除等表达式时使用。, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式例如 (12)*3} }, required: [expression] } } } ] def run_agent(user_input: str): messages [{role: user, content: user_input}] resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools, tool_choiceauto ) msg resp.choices[0].message if msg.tool_calls: messages.append(msg) for call in msg.tool_calls: func_name call.function.name args json.loads(call.function.arguments) if func_name get_weather: result get_weather(args[city]) elif func_name get_current_time: result get_current_time() elif func_name calculator: result calculator(args[expression]) else: result 未知工具 messages.append({ role: tool, tool_call_id: call.id, content: result }) second_resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools ) return second_resp.choices[0].message.content return msg.content if __name__ __main__: tests [ 北京天气怎么样, 现在几点了, 帮我算一下 (128)*5 等于多少 ] for t in tests: print(f用户: {t}) print(f助手: {run_agent(t)}\n)你看这个结构其实已经很接近一个真正的Agent了它能理解意图能选择合适的工具能执行工具还能把工具结果组织成自然语言回复。很多商业产品里的Agent核心逻辑本质上就是这样一个循环加上更丰富的工具集和更完善的记忆系统。有一点必须提醒示例里的calculator用了eval这在生产环境中是绝对不能这么干的存在代码注入风险。正式项目要么用解析库安全计算要么将参数严格校验后再执行。这种底线问题越早建立意识越好。4. 进阶核心给Agent装上规划大脑和记忆系统4.1 为什么需要任务规划没有规划的Agent只会乱撞有了工具调用能力Agent已经能完成很多任务了但你很快会发现一个新问题当一个任务比较复杂时模型往往会手足无措。比如你让它帮我准备一份关于AI行业发展的调研报告这需要多个步骤搜索资料、整理大纲、撰写内容、给出建议。如果你不做任何规划引导模型很可能直接把第一步搜索资料当成全部任务抓几条信息就完了。这边讲一个实际的测试我把一个多步骤任务丢给没有规划能力的Agent它只调用了两次搜索就草草收场输出了一份很浅的答案。后来我给Agent加了规划机制它先列出详细步骤再逐步执行每一步都检查中间结果最后输出了一份结构完整的报告。同一个模型、同一个工具集差别就是有没有规划能力。所以任务规划是Agent从能干活变成能干复杂活的分水岭。规划的本质是让Agent在开始行动之前先在脑子里形成一张任务路径图而不是走一步看一步。4.2 两种规划实现方案单次规划与动态规划规划机制在工程上有两种主流实现方式。单次规划Agent在接到任务后一次性生成一个完整的多步骤计划然后按顺序执行。这种方式的优点是结构清晰、比较容易实现适合步骤明确、流程固定的任务。缺点是不够灵活如果中途情况变化计划就不能命中实际需求难以调整。动态规划Agent每执行完一步都重新评估当前状态再决定下一步做什么。这个方案更接近人类的思考方式灵活性很高能应对复杂的动态环境。缺点是逻辑更复杂循环次数可能比较多成本也更高。实际项目中我习惯的方式是大计划、小执行先用单次规划生成总体路线图然后每一步用动态规划细化执行在执行过程中定期检查是否有必要调整计划。这种组合方式既保证了方向感又保留了灵活性。这里也提示一下一个通用Agent里规划的目标不难难点在于如何判断计划是否需要调整这往往需要一部分启发式规则或者观察模块的辅助反馈。4.3 记忆系统短期记忆与长期记忆的工程实现做Agent一定会撞上记忆问题。我把它拆成两个层次来讲对应人类的短期记忆和长期记忆。短期记忆就是当前任务上下文。技术上通常通过维护消息列表来实现。但这里有一个工程上的关键随着Agent执行步骤增加消息列表会变得越来越长你必须定期做摘要压缩否则上下文很快会塞满。长期记忆则是跨会话的知识存储。Agent在昨天和你聊过什么、你喜欢什么风格、这个问题上次是怎么解决的都应该被保存下来在后续任务中被检索并利用。工程上最常用的方案是向量数据库把文本切块向量化存入数据库在需要的时候把当前问题转成向量去数据库里做相似度检索取回最相关的历史片段拼接进上下文。我画一个简化的流程帮你理解先生成记忆 → 写入向量库 → 查询时向量化 → 检索相似片段 → 注入Prompt。很多人觉得这很难其实核心就是文本切块 向量化 相似度计算三步是有成熟工具可以快速实现的。4.4 一个带简单规划与记忆的Agent示例接下来我们看一个综合示例。这个Agent能根据任务复杂程度决定是否规划并利用一个简单的记忆文件存储历史有效信息。import json import os from openai import OpenAI client OpenAI(api_keyyour-api-key, base_urlhttps://your-model-endpoint) MEMORY_FILE memory.json def load_memory(): if os.path.exists(MEMORY_FILE): with open(MEMORY_FILE, r, encodingutf-8) as f: return json.load(f) return {} def save_memory(memory): with open(MEMORY_FILE, w, encodingutf-8) as f: json.dump(memory, f, ensure_asciiFalse, indent2) def search_web(query: str) - str: # 实际项目这里接入搜索API示意返回固定数据 return f搜索[{query}]的结果模拟搜索结果包含标题与摘要。 def summarize(text: str) - str: resp client.chat.completions.create( modelyour-model-name, messages[{role: user, content: f请总结以下内容{text}}] ) return resp.choices[0].message.content tools [ { type: function, function: { name: search_web, description: 搜索互联网获取最新信息。当用户需要查询事实、资料、新闻时使用。, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词} }, required: [query] } } } ] def run_with_planning(user_input: str): memory load_memory() plan_resp client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: 你是任务规划助手。如果任务需要多个步骤才能完成请输出一个编号列表如果任务简单输出无需规划四个字。}, {role: user, content: user_input} ] ) plan_text plan_resp.choices[0].message.content messages [ {role: system, content: f你是高效智能助手。历史记忆{json.dumps(memory, ensure_asciiFalse)}}, {role: user, content: f任务目标{user_input}\n规划建议{plan_text}} ] resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools ) msg resp.choices[0].message if msg.tool_calls: messages.append(msg) for call in msg.tool_calls: args json.loads(call.function.arguments) tool_result search_web(args[query]) messages.append({ role: tool, tool_call_id: call.id, content: tool_result }) final_resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools ) final_answer final_resp.choices[0].message.content else: final_answer msg.content memory[user_input[:20]] final_answer[:100] save_memory(memory) return final_answer if __name__ __main__: task 帮我调研人工智能在医疗领域的三个应用方向并给出简要说明 print(run_with_planning(task))这个Demo已经具备了真实Agent的很多要素任务规划、工具调用、记忆读写。你可以把它当一个骨架往里面自由扩展。比如把记忆从文件升级为向量数据库把search_web替换成真实搜索API把规划环节做得更智能。一步一步来你对Agent的理解会越来越深。5. 走向Autonomous Agent架构升级与实战经验5.1 自主智能体的完整工作循环当我们聊到Autonomous Agent指的就是能自主执行全流程、目标驱动、自我调整的智能体。它的完整工作循环我概括为六个字目标、计划、行动、观察、反思、迭代。展开说就是设定一个明确目标 → 根据目标拆解计划 → 执行计划中的具体行动 → 观察行动结果 → 根据结果反思和调整 → 迭代直到达成目标。这个循环不是线性的而是一个闭环反馈系统。在我们追过的很多开源项目里各种框架的实现细节不同但底层的循环机制是高度相似的。你理解了闭环你就理解了自主智能体的骨架。你写代码时也照着这个闭环去组织逻辑就能写出结构清晰的Agent系统。5.2 多Agent协作架构为什么要拆成多个角色单Agent能解决不少问题但在真实复杂的业务里一个Agent包打天下往往效果不佳。这时候就需要多Agent协作架构。多Agent协作的核心思想是让不同角色各司其职有的负责拆解目标有的负责具体执行有的负责审核结果有的负责记忆管理。你甚至可以让多个不同侧重点的Agent相互讨论、互相验证提高答案质量。这种架构带来的优点很明显职责清晰每个Agent做的事情专注、可控扩展性好增加一个能力就是增加一个Agent鲁棒性更高某个环节出错可以有其他角色兜底或检查。缺点则是系统复杂度大幅上升消息传递、并发控制、死锁问题、成本控制都会变得棘手。我的建议是先精通单Agent把工具、规划、记忆都搞扎实再考虑拆分布式。很多团队上来就搞一堆Agent结果互相传递消息就乱成一锅粥。根基不稳架构再花哨也是空中楼阁。5.3 实战中的六个经验教训这些是我在做了多个Agent项目后总结出来的实操心得每一条都是踩坑换来的第一个工具描述一定要详细。模型对工具的理解完全来自你写的description写得模糊模型就会该调不调或者调错。任何偷懒描述后面都会变成线上事故。第二个所有外部工具的输入必须严格校验。工具是Agent的手但也是攻击者的入口。不做参数校验、不做权限控制的工具迟早出问题。第三个上下文管理必须提前设计。不要等问题爆发再优化。Token成本、响应延迟、失忆问题根源几乎都是上下文管理没做好。第四个日志和追踪是Agent开发的刚需。Agent出错时你需要在几百行逻辑里快速定位是哪一步出了问题。没有完整的调用链日志排查会非常痛苦。第五个模型要分层使用。简单任务用快的便宜的小模型复杂任务才调动大模型。这是成本控制的核心手段。具体参考建议是比如简单分类、信息提取用轻量模型即可只有规划、总结、反思这类复杂推理才需要强模型。第六个从简单场景开始。第一个Agent项目不要挑战最高难度先做一个能查天气、能查时间的小助手把整个工程链路跑通再逐步增加复杂能力。这是最稳妥的进阶路线。5.4 下一步学习路线与常见问题排查速查表如果你准备继续深入我推荐的学习路线是先掌握Prompt工程核心技巧再深入理解Function Calling的底层协议然后学会向量数据库和RAG检索增强生成接着研究各种Agent框架的设计思路最后再尝试构建自己的多Agent系统。这里还整理了一份高频问题排查速查表给刚开始动手的你参考现象可能原因排查思路与解决建议模型不调用工具工具描述不够清晰检查description是否明确说明使用场景和触发条件适当补充示例工具参数传错Schema参数描述有歧义在parameters的description里写明参数含义、类型、取值范围和示例上下文很快超过限制没做记忆压缩和管理引入摘要压缩或向量检索只保留必要上下文Agent中途死循环缺少终止条件或步骤上限设置最大迭代次数、增加观察条件循环中检测任务完成信号回复内容与工具结果不符未把工具执行结果回传模型确保工具结果通过tool消息追加到对话列表再让模型生成最终回答同一个任务的输出不稳定温度过高或缺少规划适当降低temperature增加规划与反思环节收敛随机性这份表是我自己项目里总结出来的不一定覆盖所有情况但能帮你解决起步阶段八成以上的问题。最后再分享一个小建议把这篇文章里的几个Demo代码逐个敲一遍不要复制粘贴哪怕慢一点一个个字符敲出来遇到报错就自己排查。这个过程比看十篇教程都有用。等你亲手把一个能调用工具、能做规划、能记东西的Agent跑起来那种感觉跟看别人演示是完全不一样的。