1. 从零搭建AI Agent,先搞清楚它到底是个什么东西
很多人第一次听到“AI Agent”这个词,脑子里浮现的是科幻电影里那种能自己思考、自己行动的机器人。其实没那么玄乎。我做了几个Agent项目之后,最直观的理解是:AI Agent就是一个能自己决定“下一步做什么”的程序。它跟普通的脚本或者工作流最大的区别在于,脚本是你写死了第一步干什么、第二步干什么,而Agent是给它一个目标,它自己判断该调用什么工具、该查什么资料、该什么时候停下来。
那它跟LLM又是什么关系?这个问题我被问过不下几十次。简单说,LLM(大语言模型)是Agent的“大脑”,但光有大脑不够。你想想,一个人只有大脑没有手脚,能干活吗?Agent就是给LLM装上了手脚——工具系统让它能查天气、能搜网页、能读文件、能执行代码;记忆系统让它能记住之前聊过什么;规划能力让它能把一个大任务拆成若干小步骤。所以常说的DeepSeek、GPT这类,它们本身是LLM,是模型,不是Agent。你把LLM套上一个循环逻辑,再给它配上工具,它才变成Agent。
这篇文章适合谁看?如果你写过一点Python,知道变量、函数、循环是怎么回事,但没接触过Agent开发,那正好。如果你连Python都没装过,也没关系,我会把安装步骤写得足够细。整篇内容我会用一个具体的例子贯穿:做一个能查天气、能算数学、能搜索本地文件的个人助理Agent。这个例子足够简单,能让你在一两个小时内跑通,但又涵盖了Agent开发的全部核心环节。
注意:搭建Agent不需要什么高端显卡,你手头的笔记本就能跑。我们调用的是云端LLM的API,本地只负责逻辑编排。
2. 环境准备:Python和依赖库的安装
2.1 Python安装,别在这第一步就卡住
Windows用户直接去Python官网下载安装包,版本选3.10或3.11都行,别选太新的,有些库还没跟上。安装的时候有一个关键操作:勾选“Add Python to PATH”。我见过太多人装完Python,在命令行敲python提示“不是内部或外部命令”,就是因为没勾这个。如果你忘了勾,重新运行安装程序,选“Modify”,把那个选项补上就行。
macOS用户稍微省心一点,系统自带Python,但版本可能偏旧。建议用Homebrew装一个:brew install python@3.11。Linux用户更不用说了,包管理器一行命令的事。装完之后验证一下:
python --version pip --version两条命令都能输出版本号,说明环境没问题。如果pip提示找不到,试试python -m pip --version,这俩是等价的。
2.2 虚拟环境,别嫌麻烦
我强烈建议每个项目都建一个独立的虚拟环境。原因很简单:不同项目依赖的库版本可能冲突,全局安装迟早出问题。创建和激活的命令:
python -m venv agent-env # Windows agent-env\Scripts\activate # macOS / Linux source agent-env/bin/activate激活之后,命令行前面会出现(agent-env)的标识。以后所有pip install都装在这个环境里,跟系统Python隔离开。
2.3 核心依赖库,就装这几个
我们需要的库不多,但每一个都有明确用途:
pip install openai requests python-dotenvopenai:调用LLM API的官方库,兼容大多数主流模型服务。requests:发HTTP请求,查天气、搜网页都靠它。python-dotenv:管理API密钥等敏感信息,后面会详细讲。
如果你打算做更复杂的Agent,可以再加langchain或者llama-index,但第一个Agent我建议手写核心循环,别一上来就用框架。框架帮你省了事,但也把细节藏起来了,出了问题你都不知道从哪查。
实操心得:装库的时候如果遇到网络超时,可以加国内镜像源,比如
pip install openai -i https://pypi.tuna.tsinghua.edu.cn/simple。这个操作不涉及任何敏感内容,纯粹是加速下载。
3. 核心设计:ReAct模式为什么适合第一个Agent
3.1 ReAct是什么,用大白话讲
ReAct是“Reasoning + Acting”的缩写,翻译过来就是“推理加行动”。它的核心逻辑是一个循环:想一步,做一步,看结果,再想下一步。举个例子,你问Agent“北京今天适合穿什么衣服”,它的思考过程是这样的:
- 思考:我需要知道北京今天的天气。
- 行动:调用天气查询工具,参数是“北京”。
- 观察:工具返回“晴,15到25摄氏度”。
- 思考:温度适中,晴天,建议穿薄外套或长袖。
- 回答:北京今天晴,15到25度,建议穿薄外套。
这个循环可以重复多次,直到Agent认为任务完成,输出最终答案。为什么选ReAct而不是别的模式?因为它最直观,最接近人类解决问题的过程,而且实现起来不复杂。你不需要搞什么复杂的规划算法,就是一个while循环加上LLM的调用。
3.2 Agent和普通LLM调用的本质区别
普通调用LLM是这样的:你发一条消息,它回一条消息,结束。Agent调用是这样的:你发一条消息,它可能回一个“我要调用工具”的指令,你执行工具把结果喂回去,它再回一个“我还要调用另一个工具”,你再执行,如此往复,直到它说“这是我的最终答案”。
这个区别看起来小,但意义重大。普通LLM只能用它训练时学到的知识,Agent可以实时获取外部信息。普通LLM只能聊天,Agent可以真正“做事”。
3.3 工具系统的设计原则
工具就是Agent能调用的函数。设计工具时有几个原则我踩过坑之后总结出来的:
- 一个工具只做一件事。别搞一个“万能工具”什么都能干,LLM会懵的。
- 参数要少而明确。最好不超过三个参数,每个参数的类型和含义都要在描述里写清楚。
- 返回值要简洁。别返回一大坨JSON,LLM解析起来费劲,还浪费token。
- 错误处理要友好。工具执行失败时,返回一个人类能看懂的提示,而不是一堆报错堆栈。
我们这次做三个工具:查天气、算数学、搜本地文件。每个工具对应一个Python函数,函数上面写好文档字符串,LLM会根据文档字符串来判断什么时候调用哪个工具。
4. 实操过程:一步步把Agent搭起来
4.1 项目结构,先搭个架子
在开始写代码之前,先把目录结构定好。我习惯这样组织:
my-agent/ ├── .env ├── agent.py ├── tools.py └── requirements.txt.env:存放API密钥,不提交到代码仓库。agent.py:主程序,包含Agent循环逻辑。tools.py:所有工具函数的定义。requirements.txt:依赖列表,方便复现环境。
4.2 密钥管理,这一步绝对不能省
把API密钥硬编码在代码里,然后不小心传到公开仓库,这种事我见过太多次了。正确做法是用.env文件:
LLM_API_KEY=你的密钥 LLM_BASE_URL=你的API地址然后在代码里这样读取:
import os from dotenv import load_dotenv load_dotenv() api_key = os.getenv("LLM_API_KEY") base_url = os.getenv("LLM_BASE_URL").env文件要加到.gitignore里,确保不会被提交。如果你用的是共享电脑,还可以考虑用系统环境变量,但.env对个人项目来说足够方便。
注意:任何时候都不要把密钥打印到日志里,也不要在报错信息里暴露密钥。有些库的报错会带上请求头信息,记得检查一下。
4.3 工具函数的实现
先写tools.py。每个工具函数都要有清晰的文档字符串,这是给LLM看的“说明书”。
import requests import os def get_weather(city: str) -> str: """查询指定城市的当前天气。 Args: city: 城市名称,例如"北京"、"上海"。 Returns: 天气描述字符串,包含温度和天气状况。 """ # 这里用一个免费的天气API做示例 # 实际使用时替换成你申请的API try: url = f"https://api.example.com/weather?city={city}" resp = requests.get(url, timeout=5) data = resp.json() return f"{city}当前天气:{data['condition']},温度{data['temp']}摄氏度" except Exception as e: return f"查询天气失败:{str(e)}" def calculate(expression: str) -> str: """计算数学表达式。 Args: expression: 数学表达式字符串,例如"2 + 3 * 4"。 Returns: 计算结果字符串。 """ try: # 只允许基本数学运算,防止代码注入 allowed = set("0123456789+-*/.() ") if not all(c in allowed for c in expression): return "表达式包含不允许的字符" result = eval(expression) return f"计算结果:{result}" except Exception as e: return f"计算失败:{str(e)}" def search_files(keyword: str, directory: str = ".") -> str: """在指定目录下搜索包含关键词的文件。 Args: keyword: 搜索关键词。 directory: 搜索目录,默认为当前目录。 Returns: 匹配的文件列表,最多返回10个。 """ matches = [] for root, dirs, files in os.walk(directory): for f in files: if keyword.lower() in f.lower(): matches.append(os.path.join(root, f)) if len(matches) >= 10: break if len(matches) >= 10: break if matches: return "找到以下文件:\n" + "\n".join(matches) return "没有找到匹配的文件"这三个函数都很简单,但覆盖了Agent工具系统的典型场景:网络请求、本地计算、文件操作。
4.4 Agent主循环,核心中的核心
agent.py是整个项目的灵魂。我先把完整代码放出来,然后逐段解释。
import json import os from openai import OpenAI from dotenv import load_dotenv from tools import get_weather, calculate, search_files load_dotenv() client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL") ) # 工具注册表:名称 -> 函数 TOOLS = { "get_weather": get_weather, "calculate": calculate, "search_files": search_files } # 工具描述,告诉LLM有哪些工具可用 TOOL_DESCRIPTIONS = """ 你可以使用以下工具: 1. get_weather(city: str) - 查询指定城市的天气 2. calculate(expression: str) - 计算数学表达式 3. search_files(keyword: str, directory: str) - 搜索文件 当你需要使用工具时,请按以下格式输出: THOUGHT: 你的思考过程 ACTION: 工具名称 ARGS: JSON格式的参数 当你不需要使用工具,可以直接回答时,请按以下格式输出: THOUGHT: 你的思考过程 ANSWER: 你的最终回答 """ def run_agent(user_input: str, max_steps: int = 5) -> str: """运行Agent主循环。""" messages = [ {"role": "system", "content": TOOL_DESCRIPTIONS}, {"role": "user", "content": user_input} ] for step in range(max_steps): # 调用LLM response = client.chat.completions.create( model="your-model-name", messages=messages, temperature=0 ) reply = response.choices[0].message.content print(f"--- 第{step+1}步 ---") print(reply) # 解析LLM的输出 if "ANSWER:" in reply: answer = reply.split("ANSWER:")[-1].strip() return answer if "ACTION:" in reply: try: action_line = reply.split("ACTION:")[1].split("\n")[0].strip() args_line = reply.split("ARGS:")[1].split("\n")[0].strip() args = json.loads(args_line) # 执行工具 if action_line in TOOLS: result = TOOLS[action_line](**args) else: result = f"未知工具:{action_line}" # 把结果喂回给LLM messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"工具执行结果:{result}"}) except Exception as e: messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"解析失败:{str(e)},请重新输出"}) else: # 没有ACTION也没有ANSWER,直接返回 return reply return "达到最大步数限制,任务未完成" if __name__ == "__main__": while True: user_input = input("\n你:") if user_input.lower() in ["exit", "quit"]: break answer = run_agent(user_input) print(f"\nAgent:{answer}")这段代码的核心逻辑就是一个for循环,最多跑max_steps轮。每一轮把当前的消息历史发给LLM,LLM返回文本,我们解析文本里有没有ACTION或ANSWER。有ACTION就执行工具,把结果追加到消息历史里,进入下一轮。有ANSWER就直接返回。
4.5 提示词的设计细节
TOOL_DESCRIPTIONS这个系统提示词非常关键。它做了三件事:告诉LLM有哪些工具可用、每个工具的参数是什么、输出格式应该长什么样。格式约定用THOUGHT、ACTION、ARGS、ANSWER这几个标记,是为了方便程序解析。
为什么用这种自定义格式而不是OpenAI的function calling?因为function calling需要特定的API支持,不是所有模型都兼容。自定义文本格式虽然土一点,但通用性强,你换任何模型都能跑。等你熟悉了基本流程,再去用function calling或者框架提供的高级功能,会理解得更透彻。
实操心得:
temperature设成0,让LLM的输出尽量确定。Agent场景下不需要创造力,需要的是稳定和可预测。
5. 跑起来之后,你会遇到的那些坑
5.1 LLM不按格式输出怎么办
这是最常见的问题。你明明在提示词里写了“请按THOUGHT/ACTION/ARGS格式输出”,但LLM有时候就是自由发挥,给你回一段散文。我的处理办法是在解析失败时,把错误信息喂回去让它重试:
messages.append({"role": "user", "content": "你的输出格式不正确,请严格按照THOUGHT/ACTION/ARGS或THOUGHT/ANSWER格式重新输出。"})通常重试一两次就能纠正。如果反复失败,可能是提示词不够明确,或者模型能力太弱。换个模型试试。
5.2 工具参数解析失败
json.loads经常因为LLM输出的JSON格式不对而报错,比如多了个逗号、少了引号。可以在提示词里强调“ARGS必须是合法的JSON”,同时在代码里做容错:
try: args = json.loads(args_line) except json.JSONDecodeError: # 尝试修复常见问题 args_line = args_line.replace("'", '"') args = json.loads(args_line)5.3 Agent陷入死循环
有时候Agent会反复调用同一个工具,比如一直查天气查个不停。max_steps参数就是防这个的。另外可以在提示词里加一句“如果已经获得足够信息,请直接给出ANSWER”。如果还是循环,检查一下工具返回的结果是不是让LLM误以为需要再查一次。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 提示“不是内部或外部命令” | Python未加入PATH | 重新安装并勾选Add to PATH |
| pip安装超时 | 网络问题 | 加国内镜像源 |
| LLM返回401错误 | 密钥错误或过期 | 检查.env文件中的密钥 |
| 工具执行结果为空 | 工具函数报错被吞 | 在工具函数里加详细日志 |
| Agent不调用工具 | 提示词不够明确 | 强化工具描述和格式要求 |
| 输出乱码 | 编码问题 | 确保文件保存为UTF-8 |
6. 进阶方向:这个Agent还能怎么玩
6.1 加入记忆系统
现在的Agent每次对话都是独立的,不记得之前聊过什么。加一个简单的记忆系统就能解决:把历史对话存到一个列表里,每次调用LLM时把最近N轮对话一起发过去。更复杂的做法是用向量数据库做长期记忆,把重要信息存起来,需要时检索出来。
6.2 接入更多工具
工具系统是Agent能力的边界。你可以接入搜索引擎、数据库、邮件发送、日历管理等等。每加一个工具,就在TOOLS字典和TOOL_DESCRIPTIONS里注册一下。注意工具多了之后,提示词会变长,token消耗会增加,需要权衡。
6.3 多Agent协作
一个Agent干不完的活,可以拆给多个Agent。比如一个负责规划,一个负责执行,一个负责检查。每个Agent有自己的提示词和工具集,通过消息传递来协作。这个方向就比较复杂了,建议先把单Agent玩熟再说。
6.4 用框架加速开发
当你手写了几遍Agent循环之后,可以试试LangChain、LlamaIndex这些框架。它们把工具调用、记忆管理、多Agent协作都封装好了,开发效率会高很多。但前提是你理解底层原理,不然出了问题只能干瞪眼。
我个人在实际操作中的体会是:第一个Agent一定要手写,哪怕代码丑一点、功能少一点。手写一遍之后,你对ReAct循环、工具调用、提示词设计的理解会深刻得多。后面再用框架,就是如虎添翼,而不是囫囵吞枣。
最后分享一个小技巧:调试Agent的时候,把每一步的LLM输出都打印出来。这样你能清楚地看到Agent在想什么、为什么调用这个工具、为什么给出这个答案。这个习惯帮我省了无数排查时间。