人生第一个AI智能体跑通的那一刻我盯着终端里那句“好的已帮你把明天的会议提醒加进待办”愣了几秒。代码量并不大一个循环、几张工具表、一段系统提示词但它确实能听懂自然语言自己决定查天气、记待办、整理结果。我把这个智能体叫作阿智。这篇文章不讲多智能体编排也不讲复杂框架只记录一个AI智能体从零到一的完整过程怎么选模型、怎么定义工具、怎么写主循环以及上线两周内踩过的一堆坑。想动手做第一个Agent、又被各种概念绕晕的新手这篇应该能帮你省不少时间。1. 动手之前先把AI智能体拆成三个问题1.1 智能体和聊天机器人差的不是能力而是“手”很多人一开始会把AI智能体和聊天机器人搞混我一开始也是。聊天机器人做的是“输入一句话输出一段话”它再聪明也碰不到外部世界。而智能体最大的区别是它有一双可以执行动作的“手”。这双手不是一个具体硬件而是一批预先定义好的函数——查天气、加待办、读文件、发消息。模型负责判断什么时候用哪只手、用什么参数代码负责把这只手真正伸出去执行执行完再把结果交回给模型继续思考。这个区别让我少走了很多弯路。刚开始我做的是纯聊天式机器人模型回答得再漂亮也没法真正帮我干成一件事。后来加了一个查询函数模型才能从“嘴上说要查”变成“真的去查”。我后来给朋友解释时常用一个类比聊天机器人是只出主意的朋友智能体是能顺手把事办了的助理。你需要的不是又一个只会说话的窗口而是一个能自己调用工具完成任务的闭环。1.2 我选的第一个场景查询、提醒、记录三件套选第一个场景很关键。我见过不少人一上来就想做个全能助理结果模型、工具、记忆、权限全搅在一起半个月还在原地打转。我的原则是第一个AI智能体只解决三类小事。天气查询模型根据城市名调用天气工具返回今日温度、天气状况和穿衣建议。待办记录用户说“明天下午三点开会”模型解析出时间和事项写入待办清单。事项查询用户问“我这周有哪些待办”模型从待办存储里读取并整理成列表。这三件事看似简单其实覆盖了AI智能体的核心要素自然语言解析、工具调用、参数生成、结果回填。天气结果来自外部接口充满不确定性待办记录需要写入状态事项查询需要读取历史数据。每一种情况都对应一种真实的图景但整体复杂度又足够低单个函数最多十行代码。更重要的是这是我每天都会用到的场景动机足够强不会做到一半放弃。1.3 第一版的边界清单先学会做减法决定做什么之前我先明确不做什么。这个步骤非常值得写进每一个新手的计划表里。不做自主联网搜索。搜索结果的噪声太大第一版没必要同时处理检索和工具调用。不做长期记忆。第一版只保留当前对话内的上下文不做向量库、不做用户画像。不做多智能体协作。一个智能体加三个工具已经是很好的起步编排多个角色只会让问题翻倍。不做语音和前端界面。全部通过命令行和HTTP接口交互把复杂度控制在最底层。边界清单的用处是防止你被“智能体”这个概念绑架。AI智能体的本质是一个有工具、有状态、有反馈循环的程序先把循环跑顺再谈扩展。我见过太多卡在第一步的人问题不在能力不够而是功能范围铺得太大任何一个环节出错都无法定位。第一版只做三件事反而让我在两周内就把完整闭环跑通了。2. 技术选型怎么做函数调用是智能体的命根子2.1 为什么我坚持选支持函数调用的模型技术选型是整个项目里最不能偷懒的一步。我当时对比了两条技术路线区别非常大。第一条路线是“提示词法”在系统提示词里规定模型输出固定格式的JSON比如“如果用户想查天气请输出{tool:get_weather,args:{city:北京}}”然后代码拿到这段JSON再自己解析执行。听起来可行但实际用起来很痛苦。模型经常在JSON前后加解释性文字参数名偶尔变形字段类型也会漂移有时候一个斜杠或一个换行就能让整段解析报错。调试过程基本是在跟模型的随机性搏斗。第二条路线是“原生函数调用”让模型平台在接口层面支持结构化的工具声明模型回复时如果决定调用工具会返回一个结构化的tool_call对象包含工具名称和标准JSON格式的参数代码不需要再去猜。这条路稳得多模型的选择准确率和参数规范程度都明显更好。我在实测中对比了一下同样的需求原生函数调用的参数解析成功率接近满分而提示词法会有大量“看起来差不多但就是跑不对”的情况。所以我很明确如果要做AI智能体第一步就是确认你选的模型API支不支持原生函数调用。这是后续所有稳定性的基础很难事后弥补。2.2 极简三层结构模型层、工具层、状态层整个项目的架构我没有做得太复杂就是三层层级职责第一版的选择模型层理解用户意图、判断是否调用工具、生成参数、汇总回答支持原生函数调用的商用大模型API工具层真正执行动作比如调天气接口、写待办文件纯Python函数统一注册成工具表状态层保存多轮消息、工具调用记录、临时记忆用messages列表保存带长度控制这三层看起来简单却是大多数失败项目忽略的东西。模型层要回答的核心问题是“用户想干什么”工具层要回答“这件事具体怎么落地”状态层要回答“我们聊到哪一步了”。只要你能把这三层分清楚后面加功能就是往工具层里塞函数、往状态层里加存储的问题。我当时没有引入任何数据库待办事项就写在内存列表里重启会丢但对验证流程完全够用。我也特意没有在工具层做统一调度框架每个函数就是普通Python函数用字典按名字映射。简单到不能再简单出问题时一眼就能看到底层逻辑。2.3 自己写主循环还是套开源框架市面上有不少开源编排框架封装了记忆、工具调用缓存、重试、Agent循环这些功能。我也试过体验是“省事但不好查错”。因为框架帮你干的事太多了一旦某个环节出了问题你得跟着它内部的抽象层一层一层挖。第一次做智能体的人不太适合在这种黑盒状态下学习。所以我最终的方案是自己写主循环。核心循环其实就是这几步把历史消息发给模型附带工具Schema。模型返回结果要么是纯文本回答要么是一个或多个tool_call请求。如果是tool_call代码去执行对应函数把结果包装成tool消息回填给模型。模型看到工具结果后继续思考再决定继续调用工具还是输出最终回答。重复直到模型直接给出文本回答或者达到最大步数。用这个思路写下来核心代码只有三十行左右。自己手写这个循环不是为了炫技而是为了让你能清楚地看到“模型决策”和“真实执行”之间来回交替的过程。理解了这个节奏后面再用任何框架都能快速上手。3. 从零到跑通第一个Agent的完整实操记录3.1 第零步环境、接口与密钥管理动手前先把环境准备好。我用的Python版本是3.10以上用一个虚拟环境隔离依赖。模型部分通过官方Python SDK请求对话接口SDK本身不复杂本质上就是封装了HTTP请求和响应解析。你只要注册账号、创建一个API Key然后把Key配置到环境变量里就行。如果你用的操作系统是Linux或macOS在终端里这样设置export MODEL_API_KEYsk-这里换成你的Key export MODEL_NAME你的模型代号这里有一个我从第一天就坚持的原则API Key绝不写死在代码里。项目代码在几周后大概率会被你推到代码仓库哪怕仓库是私有的也不值得冒泄露风险。正确做法是只从环境变量读取还顺手把.env文件加入.gitignore。import os API_KEY os.getenv(MODEL_API_KEY) MODEL_NAME os.getenv(MODEL_NAME) if not API_KEY: raise RuntimeError(没有找到MODEL_API_KEY环境变量)另外提醒一句第一版不需要买太贵的模型普通的、支持函数调用的商用模型足以完成天气查询和待办记录。模型贵不等于Agent更聪明对第一版来说稳定和便宜更重要。3.2 第一步写一个真正能约束行为的系统提示词系统提示词不是摆设它决定了模型在“自由发挥”和“按规则办事”之间的位置。我的第一版系统提示词反复改了很多次最终沉淀下来这几个要点说明自己是什么一个生活助理型AI智能体能用工具完成任务。列举可用工具把能调用的工具名称和用途说清楚。明确触发条件用户没有明确要求时不要调用工具。明确失败处理工具调用失败就实话实说不要编造结果。明确回答格式拿到工具结果后用几句自然语言总结给用户。实际提示词大概是这样的你是阿智一个生活助理型AI智能体。 你可以使用以下工具 1. get_weather查询某个城市的实时天气需要传入城市名。 2. add_todo新增一条待办事项需要传入事项内容和时间。 行为守则 - 只有用户明确提出相关请求时才使用工具不要擅自调用。 - 调用工具前必须把参数提取准确。参数缺失时主动向用户确认。 - 如果工具返回错误直接告知用户失败原因不要自行推测。 - 拿到工具结果后用一段简洁的自然语言回复用户。这套提示词的价值在于减少模型“自作主张”。第一次写的时候我漏了“参数缺失时主动确认”这条结果模型遇到“查天气”这种没有城市名的请求会自己编一个城市去查。加上这条之后它才会反问用户“请问你要查哪个城市”。这一个小小的提示词改动效果比换一个大模型还要明显。3.3 第二步把工具函数和参数描述注册成Schema工具层的核心是把Python函数“翻译”成模型能理解的Schema。每个工具要告诉模型三件事这个工具叫什么、是干什么的、参数有哪些、每个参数是什么格式。先写真实函数。天气查询我先用模拟数据只为了验证流程import json import random # 工具函数全部存到一个字典里 TOOL_FUNCTIONS {} def get_weather(city: str) - dict: 查询某个城市今天的天气真实项目中可换成天气API weather_list [晴, 多云, 小雨] weather random.choice(weather_list) temperature_c random.randint(16, 32) return { city: city, weather: weather, temperature_c: temperature_c, source: mock } TOOL_FUNCTIONS[get_weather] get_weather TODO_STORE [] def add_todo(content: str, due: str ) - dict: 新增一条待办事项 item {content: content, due: due} TODO_STORE.append(item) return {status: ok, id: len(TODO_STORE) - 1, item: item, total_count: len(TODO_STORE)} TOOL_FUNCTIONS[add_todo] add_todo然后定义工具Schema。这里要严格按照模型平台的JSON Schema格式来写我第一版就是在这里漏了required字段导致模型有时不传必填参数TOOL_SCHEMAS [ { type: function, function: { name: get_weather, description: 查询某个城市的实时天气返回天气状况和温度。, parameters: { type: object, properties: { city: { type: string, description: 城市名例如北京、上海、广州 } }, required: [city] } } }, { type: function, function: { name: add_todo, description: 新增一条待办事项可以带可选的时间说明。, parameters: { type: object, properties: { content: { type: string, description: 待办事项内容 }, due: { type: string, description: 可选事项时间说明例如明天下午三点 } }, required: [content] } } } ]写Schema的时候要注意一点描述越具体模型生成参数的准确率越高。比如“城市名”这种描述比不写要可靠得多。实测下来描述详细的城市名示例能显著减少模型把“北京”写成“北京市”这类小偏差。3.4 第三步5分钟看懂Agent主循环下面是整个AI智能体最核心的主循环。代码不长但它把“模型决策”和“真实执行”串成了一个完整闭环import json # client 请替换成你实际使用的模型平台SDK客户端 from llm_client import create_client client create_client(api_keyAPI_KEY, modelMODEL_NAME) def execute_tool(name: str, arguments_json: str): 根据模型返回的工具名和参数执行真实的Python函数 fn TOOL_FUNCTIONS.get(name) if fn is None: return {error: f未知工具: {name}} try: args json.loads(arguments_json) if arguments_json else {} return fn(**args) except Exception as e: return {error: f工具执行失败: {str(e)}} def run_agent(user_input: str, max_steps: int 8) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input} ] for _ in range(max_steps): resp client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) msg resp.choices[0].message # 如果模型没有要求调用工具说明它准备直接回答了 if not msg.tool_calls: return msg.content or 空回复 # 把模型这次带工具调用的回复加入历史 messages.append(msg) # 逐个执行模型请求的工具 for tc in msg.tool_calls: result execute_tool(tc.function.name, tc.function.arguments) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse) }) return 已达最大步数任务未能完成。这段逻辑的核心是模型不直接执行工具它只负责“出主意”——决定调用什么工具、传什么参数。真正的执行权在代码手里执行完的结果通过tool角色的消息回填给模型。模型看到结果后继续思考直到它认为信息足够才生成最终回复。我当时第一眼看到这个循环时觉得太简单了但跑起来之后才发现所有复杂行为都是从这个循环里长出来的。模型可以连续调用多个工具、可以根据工具结果修正下一步动作、也可以判断工具结果不合理再重试。框架层面的东西全是锦上添花这个循环才是智能体的心脏。3.5 第四步给智能体加一层最简单的记忆第一版我本来不打算做记忆但跑了几天后发现一个很现实的问题用户说“帮我查一下上海天气”模型查完并回答了然后用户又说“那后天呢”。如果不保留历史模型根本不知道“那后天呢”指代的是什么。所以在不引入数据库的前提下messages列表本身就是最简单的记忆。实现上也很直白每一轮对话结束后把这一轮的user消息和assistant回复一起追加到历史列表里下一轮继续携带。我封装了一个Memory类初始版本只是包了一层列表class SimpleMemory: def __init__(self, max_history: int 20): self.max_history max_history self.messages [] def add_user(self, content: str): self.messages.append({role: user, content: content}) def add_assistant(self, content: str): self.messages.append({role: assistant, content: content}) def add_tool_results(self, tool_messages: list): self.messages.extend(tool_messages) def get_messages(self): # 超过长度时丢最旧的非系统消息简单粗暴但不崩溃 return self.messages[-self.max_history:]但这种方案有一个明显的隐患历史无限累积会让tokens水涨船高。我后面专门做了一步当messages超过20条时把最旧的一部分丢出去只保留最近的20条。对于第一版来说这个策略有点粗暴但至少把上下文控制在了可接受范围。等到第二版我再想更聪明的摘要记忆方案。4. 连续跑了两个星期我把踩过的坑整理成了这份清单4.1 Token开销失控上下文膨胀是我遇到的第一个大坑第一个坑很快来了。智能体连续对话十几轮后输入Token量肉眼可见地飙升。问题出在工具结果上天气接口每次返回完整JSON系统提示词也不短每一轮新请求都要把全部历史重新发给模型费用和延迟都跟着上涨。解决思路有三个我全部用上了工具返回值只保留必要字段。天气返回里我原本带了一堆字段后来只留下city、weather、temperature_c。旧消息截断。超过20条就丢最旧的虽然没有长期记忆但至少不会无限膨胀。工具结果的tool消息不要全文保留太长。历史里只保留一次工具调用的结果如果后续模型多次引用摘要信息就让它用对话中的文字表述。我后来算了一笔账加上这三条限制后同等工作量的Token消耗下降了大约一半。很多人调AI智能体时只看回答质量不看Token消耗等月底账单出来才肉疼。这个坑希望你不要踩。4.2 工具空转模型为什么反复调用同一个函数第二个坑特别有意思。某一次我问阿智“北京天气怎么样”它调了天气工具得到晴、28度却迟迟不回答反而又发起一次“get_weather”请求参数还是同一个。我一开始以为是模型出了问题后来仔细看了提示词才意识到我没有明确写“拿到结果后就该回答”。模型的行为守则里只有“使用工具完成用户请求”没有说“一旦拿到足够信息就停止调用工具”于是它倾向于继续确认、继续调用。代码里虽然有max_steps8的兜底但每次都白白消耗好几轮调用。解决办法是在系统提示词里加一条硬性规则当工具已经返回了你所需的信息时直接基于这些信息回答用户不要重复调用工具。另外在极端场景下可以把tool_choice从auto改成required强制模型必须先调工具再回答。但“检查天气”这种场景用auto就好不要无脑设置成required否则用户问“你好”模型也会硬去调一下工具。4.3 JSON解析失败和幻觉参数绕不开的稳定性问题原生函数调用比提示词法稳定很多但也不是100%不会出错。我在测试中遇到过两种情况模型返回的工具参数是个合法JSON但多了一个不在Schema里的字段比如get_weather传了{city: 北京, unit: 摄氏度}。我当时的函数只接受city一个参数直接用关键字传参就会报错。模型偶尔会传一个空字符串或缺字段的参数。解决办法是在execute_tool里把参数解析和函数调用都包进try/except然后把错误信息作为工具结果返回给模型。这样模型看到出错信息后会自动修正参数重新调用相当于把错误处理交给了反馈循环本身。def execute_tool(name: str, arguments_json: str): fn TOOL_FUNCTIONS.get(name) if fn is None: return {error: f未知工具: {name}} try: args json.loads(arguments_json) if arguments_json else {} # 只保留函数签名中接受的参数过滤多余字段 import inspect allowed inspect.signature(fn).parameters.keys() filtered_args {k: v for k, v in args.items() if k in allowed} return fn(**filtered_args) except Exception as e: return {error: f工具执行失败: {str(e)}}用inspect.signature过滤参数是我后来补上的虽然有点取巧但对小工具表很管用。这种“让模型自己看错误信息并纠错”的方式比硬编码一堆校验规则灵活得多。4.4 安全护栏别让智能体拥有不受控的“手”工具层是双刃剑。模型能用工具查天气、加待办也就意味着如果有危险工具模型也会尝试调用。我自己虽然只挂了两个低风险工具但仍补了一圈安全护栏因为它们会在后续迭代里救我一命。我的第一版护栏非常简单工具白名单模型只能调用TOOL_FUNCTIONS里注册过的函数未知工具一律返回错误。高风险工具强制确认如果以后接入“删除文件”“发送邮件”这类工具必须要求用户在对话里明确确认代码里再强制校验。全链路日志每次tool_call都打印出工具名、参数和结果方便事后追溯。举一个设计例子。如果加入删除待办的工具我不会直接让模型调用就删而是先要求它返回一个确认请求def delete_todo(item_id: int, force_confirm: bool False) - dict: if not force_confirm: return { require_confirm: True, hint: 删除操作不可恢复如果用户已确认请把force_confirm设为true后重新调用 } # 真实删除逻辑 return {status: deleted, id: item_id}这样模型会先向用户确认再带着确认参数重新调用。有了这层机制即使以后接入更多敏感工具也不至于出现“用户随口说了一句话智能体就把重要数据删了”的事故。很多AI智能体翻车事故问题都出在这一步。4.5 常见问题速查表现象可能原因解决办法Token消耗增长太快历史消息和工具结果全量回传截断旧消息精简工具返回字段模型反复调用同一个工具提示词没约定“拿到结果即回答”补上行为守则设max_steps上限工具参数解析失败模型生成了多余字段try/except兜底把错误回填给模型模型编造工具结果提示词里没有禁止编造明确写“失败时如实上报不推测结果”多轮对话指代不清历史消息被丢弃保留合理长度的上下文必要时摘要API Key泄露风险Key写死在代码里放到环境变量加入.gitignore这张表也是我做下一个智能体时的检查清单。每一条都是真金白银换来的建议你贴在项目文档最前面。5. 做完第一个Agent我的几点复盘和下一步计划5.1 智能体最难的从来不是模型而是边界阿智用到现在最让我意外的不是模型变聪明了而是边界设计决定了一切。三个工具、一套行为守则这个组合能完成的事情比我想象中多。反过来说如果一开始就接几十个工具系统提示词会变得臃肿不堪模型在工具选择上的准确率也会明显下降。边界设计包含几层一是工具范围二是触发条件三是失败处理四是权限控制。第一版把每一层都做得很小很明确反而让我很快摸清了智能体的所有行为模式。后面加工具时只需要沿着一层一层往里填不需要推倒重来。哪怕我现在已经想接入更多能力第一版留下的这套结构也没有过时。我也意识到智能体的回答质量不仅取决于模型还取决于“工具结果的质量”。如果天气接口返回的是杂乱的原始JSON模型再强也提炼不出好答案。所以工具层要尽可能返回结构化、语义清晰的数据这是很多教程不会强调但实际很重要的点。5.2 下一步并行工具、长期记忆与自动重试第一个版本跑通后我心里已经列了下一阶段的计划。并行工具调用让模型在一次回复里同时请求多个工具比如“查北京天气并把明天的提醒加进待办”可以同时执行效率和体验都会好很多。长期记忆准备把待办事项从内存挪到SQLite并用摘要机制把旧对话压缩成少量文本保留有价值的信息。自动重试工具调用失败时根据错误类型决定是否需要模型修正参数后重试而不是直接放弃。日志与评测把每一轮请求、工具调用、最终回答都记录下来积累一批测试用例方便后面评估改动是否回退。这些方向不算激进但都基于第一版的真实痛点。我的经验是不要等所有功能都想好再动手先把第一个闭环跑通痛点自然会告诉你下一步该做什么。5.3 给想入坑的人一个可执行的起点如果你也准备做自己的第一个AI智能体我会建议你从这样一个小目标开始选一个你每天都会遇到、能在一分钟内完成、并且需要外部信息或状态变更的小任务比如查天气、记账单、做待办、查快递。然后照着这个项目的架构从系统提示词、一个工具函数、一个主循环开始先让它在命令行里稳定跑通一百次再考虑界面和其他功能。我印象最深的一次是阿智在一个周五下午连续帮我处理了三件事查周末天气、把下周一上午的会写进待办、整理出当前待办清单。那一刻我意识到所谓AI智能体不是什么玄学它就是一个“模型决策加代码执行”的循环只是把一件很小的事完整地自动化了。愿你也能从这个循环里感受到第一次跑通时那种实实在在的兴奋。