最近总有朋友问我:AI Agent到底是什么?网上概念文章满天飞,讲得云里雾里,真到自己动手时,连一个能跑的最小Demo都没见过。我也是从这种状态过来的,最早以为Agent就是“更聪明的大模型”,后来才明白,它本质上是一套让大模型自己决定“下一步该做什么”的自动化系统。这篇就把我消化过的AI Agent核心技术用大白话讲清楚,从底层原理到可复现代码,一步步带你搭一个属于自己的Agent,零基础也能照着走。
这篇文章适合两类人:一类是刚接触大模型应用开发、想搞懂Agent原理的新手;另一类是已经会调API,但总觉得Agent“不太受控”、想深入理解工具调用和记忆机制的开发者。我不会堆术语,每个概念都会先解释“为什么”,再给“怎么做”,你跟着跑一遍,比看十篇科普都有用。
1. Agent不是“更聪明的大模型”:先搞懂它到底长什么样
1.1 一句话说透Agent:让大模型“动手干活”而非“动嘴回答”
先纠正一个最常见的误解。普通聊天机器人也好,问答应用也好,本质上是大模型根据你的输入生成一段文字。你说“帮我查一下明天天气”,它会回答“我无法实时查询天气,建议你打开天气App”。这不是模型变笨了,而是它本身就只有“生成文字”这一个能力。
Agent不一样。Agent的核心是在模型生成文字的基础上,增加“调用工具”“观察结果”“再次决策”的能力。同样是“帮我查一下明天天气”,Agent会先意识到自己不知道实时天气,于是调用一个天气查询接口,拿到数据后再组织成一句人话回复你。大模型还是那个大模型,但它从“动嘴回答”变成了“动手干活”。
你甚至可以这样理解:传统大模型像一个只会纸上谈兵的顾问,Agent则是给这个顾问配了双手、工具和执行力,让他真的去把事办了。
1.2 Agent的三个核心零件:大脑、工具、记忆
一个完整的Agent,我习惯拆成三部分:
| 零件 | 作用 | 通俗类比 |
|---|---|---|
| 大模型(LLM) | 理解意图、拆解任务、决定下一步 | 大脑和项目经理 |
| 工具(Tools/Function Calling) | 获取实时数据、执行动作、计算结果 | 双手和外部资源 |
| 记忆(Memory) | 保存对话历史、长期用户偏好 | 小本子和档案柜 |
大模型负责“思考”,工具负责“执行”,记忆解决“遗忘”。这三者缺一不可。没有工具,模型只能凭空编造;没有记忆,每次对话都是“陌生人见面”;没有大模型,前面两个只是死代码。
小白最容易忽略的是“工具”这一环。很多人觉得Agent神奇,以为是模型自己学会了搜索、计算、操作软件。其实模型依然只会输出文字,但它输出的文字是结构化的工具调用指令,你的代码拿到指令后去执行真正的函数,再把结果塞回给模型。模型没变强,变强的是这个“指令-执行-反馈”的循环。
1.3 一个循环看懂Agent的工作方式:感知-决策-行动-观察
Agent的每一次工作,都可以用四个词概括:
- 感知:读取用户当前问题,以及此前积累的记忆。
- 决策:大模型判断该直接回答,还是先调用某个工具。
- 行动:代码执行对应的工具函数,拿到真实结果。
- 观察:将结果作为“工具消息”重新喂给大模型,让它继续决策。
这个过程会一直循环,直到模型认为信息足够了、可以给出最终答案为止。
我最早看ReAct相关的论文时,被“Reasoning and Acting”这种词绕得头晕。后来自己写了一遍循环,发现其实就是“想一步,做一步,看一眼结果,再想下一步”。你不需要记什么复杂公式,只要记住这篇文章后面写的那个while循环,Agent的原理就已经掌握一半了。
2. Agent背后的四个支柱:从模型调用到Function Calling
2.1 大模型API:你不需要自己训练,只需要选对模型
训练一个自己的大模型对普通人来说既不现实也没必要。现在主流的大模型服务商都提供API调用,你只需要申请密钥、安装SDK、传几条消息,就能拿到模型生成的回复。这里我建议新手直接选支持Function Calling(函数调用)的模型,这是Agent能调用工具的关键能力。
选模型时不用纠结“哪个最强”,先看三点:是否支持工具调用、上下文窗口多大、单价多高。Agent应用会频繁调用模型,一次任务可能要来回好几轮,Token消耗比普通问答大得多。我自己的经验是,开发调试阶段用便宜的小模型,跑通逻辑后再换更强的模型,能省不少冤枉钱。
有一个细节容易踩坑:不同服务商的API地址和模型名不一样。如果你用的是兼容接口,记得同时配置 base_url;如果只是官网标准接口,就只填API Key即可。下面的代码我会用环境变量做配置,避免把密钥硬编码进代码。
2.2 System Prompt:Agent的“角色说明书”
同样一个模型,你给它不同的系统提示词,它会表现出完全不同的行为。对Agent来说,System Prompt不是随便写几句话,而是定义边界和规则。
一个合格的Agent系统提示词至少应该包含:
- 角色定位:你是什么助手,服务对象是谁。
- 能力边界:你能调用哪些工具,什么情况下必须调用工具。
- 回答风格:简洁还是详细,中文还是英文。
- 防幻觉要求:工具没返回时,不允许编造数据。
举个例子,我的某个信息助手系统提示词会写:“当用户需要实时数据或数学计算时,你必须调用对应工具。如果工具调用失败,明确告诉用户失败原因,而不是猜测一个结果。”这句话能极大减少模型胡说八道的概率。
2.3 Function Calling:真正把“工具”交到模型手里
Function Calling是Agent技术里最核心的一环,但它的原理比听起来简单。
你先把工具的描述以JSON Schema的形式传给模型。模型看到用户问题后,如果判断需要某个工具,就会在返回内容里带上一个特殊的字段tool_calls,里面包含工具名和参数。比如模型返回:“调用calculate,参数{'expression': '23*17+6'}”。你的代码解析这个字段,执行本地对应的函数,再把结果以“工具消息”的形式回传给模型。
这么做有个巨大的好处:工具本身的代码完全由你掌控。模型不能直接执行危险操作,它只能“建议”调用某个工具。要不要真执行、执行前要不要校验,都由你的代码决定。这正是Agent安全可控的核心原因。
我还记得第一次跑通Function Calling时的感受:看到模型真的把用户的问题拆成了工具调用参数,有种“原来如此”的顿悟感。Function Calling不是魔法,它就是一次结构化的输出约定。
2.4 上下文与记忆:Agent“忘事”的根本原因
大模型本身没有记忆。它每次调用都是“无状态”的,你能让它看起来有记忆,是因为你把历史消息全部塞进了请求的messages列表里。很多小白一开始不理解,为什么Agent聊着聊着就忘了前面的话——因为你没把历史消息保存并重新传给它。
这也是上下文窗口概念的由来。模型能接受的输入长度有限,超过窗口就会被截断或报错。Agent在多次调用工具时,每轮都会新增大量消息,所以上下文爆炸是实战中最常见的问题之一。后面我会专门讲怎么应对。处理记忆实际上是在“决定塞什么历史给模型”,而不是让模型自己去记住。
3. 零基础复现一个会搜索资料并自动整理答案的Agent
理论说再多不如跑个真东西。这一节我带你自己写一个最小可运行的Agent,它能算数学题、查当前时间、在本地资料库里搜索文章标题,然后综合结果回答用户提问。完整代码全部贴出,你复制粘贴,配上环境变量就能跑。
3.1 环境准备:3分钟跑通大模型调用
首先安装依赖:
pip install openai python-dotenv在项目目录创建.env文件,填入你所用大模型服务商的信息:
LLM_API_KEY=你的密钥 LLM_BASE_URL=你的接口地址(官方标准接口可以留空) MODEL_NAME=你所用的模型名先写一小段代码验证调用链路:
import os from openai import OpenAI client = OpenAI( api_key=os.environ.get("LLM_API_KEY"), base_url=os.environ.get("LLM_BASE_URL") or None, ) MODEL = os.environ.get("MODEL_NAME") response = client.chat.completions.create( model=MODEL, messages=[{"role": "user", "content": "你好,请回复'链路通了'"}], ) print(response.choices[0].message.content)能正常打印文字,说明环境没问题。这里有个小建议:第一次调试千万别用复杂模型,先用便宜的、支持工具调用的普通模型,避免烧钱。
3.2 注册你的第一批工具:计算、查时间、搜资料
工具的本质就是一个普通Python函数。我先写三个:
import ast import operator import datetime import json def calculate(expression: str) -> str: """只允许四则运算和括号的简易计算器,避免直接用 eval 带来的安全风险。""" allowed_ops = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, } def evaluate(node): if isinstance(node, ast.Expression): return evaluate(node.body) if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.BinOp): left = evaluate(node.left) right = evaluate(node.right) op_type = type(node.op) if op_type in allowed_ops: return allowed_ops[op_type](left, right) raise ValueError("不支持的表达式") return str(evaluate(ast.parse(expression, mode="eval"))) def get_current_time(timezone_offset: int = 0) -> str: """返回指定时区的当前时间,timezone_offset 是相对UTC的小时偏移。""" now = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=timezone_offset) return now.strftime("%Y-%m-%d %H:%M:%S") NEWS_DB = { "人工智能": ["AI Agent入门指南", "大模型函数调用实践", "工具调用与检索增强对比"], "天气": ["周末降温提醒", "台风路径更新"], } def search_articles(keyword: str) -> dict: """在本地资料库中检索与关键词相关的文章标题,模拟真实搜索接口。""" matched = [] for key, titles in NEWS_DB.items(): if key in keyword: matched.extend(titles) if not matched: matched = ["没有找到相关结果,可以尝试换一个更宽泛的关键词"] return {"keyword": keyword, "results": matched}这里有个安全设计值得多说一句:计算器函数我用ast模块解析表达式,而不是直接eval()。如果直接eval,用户让模型算一个“删除文件”的表达式也不是没可能。后面讲坑的时候我还会展开,凡是Agent要调用的工具,都要默认按“会被恶意输入攻击”来设计。
接下来把这些工具注册成Schema:
TOOLS = [ { "type": "function", "function": { "name": "calculate", "description": "计算数学表达式,例如 '23*7 + 9'", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "要计算的数学表达式"} }, "required": ["expression"] } } }, { "type": "function", "function": { "name": "get_current_time", "description": "获取某个时区的当前时间,参数是相对UTC的偏移小时数", "parameters": { "type": "object", "properties": { "timezone_offset": {"type": "integer", "description": "相对UTC的时区偏移,东八区为8", "default": 0} } } } }, { "type": "function", "function": { "name": "search_articles", "description": "在本地资料库中检索与关键词相关的文章标题。当你需要获取资料或用户提到某个主题时使用", "parameters": { "type": "object", "properties": { "keyword": {"type": "string", "description": "检索关键词,建议使用简短名词短语"} }, "required": ["keyword"] } } } ] TOOL_MAP = { "calculate": calculate, "get_current_time": get_current_time, "search_articles": search_articles, }描述里的每一个字都值得推敲。比如计算器工具的 description 我会写明具体格式,否则模型可能传“23乘以17加6”这种中文表达式进来。模型的参数是它自己“理解”出来的,描述越清晰,参数越准确。
3.3 核心循环:让大模型决定“下一步调用哪个工具”
下面是Agent真正的主循环。逻辑很简单:把消息发给模型,检查返回里有没有tool_calls。有就执行工具,把结果回填给模型;没有就当作最终答案返回。
from openai import OpenAI import os import json client = OpenAI( api_key=os.environ.get("LLM_API_KEY"), base_url=os.environ.get("LLM_BASE_URL") or None, ) MODEL = os.environ.get("MODEL_NAME") def run_agent(user_input: str, max_steps: int = 6) -> str: messages = [ { "role": "system", "content": "你是一个自动化信息助手。当用户的问题需要实时数据、数学计算或资料检索时,你必须调用对应工具,不要自己编造结果。工具返回后,请结合工具结果给用户一个简洁直观的回答。", }, {"role": "user", "content": user_input}, ] for step in range(max_steps): response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto", ) message = response.choices[0].message if not message.tool_calls: print(f"[第{step+1}轮] 模型决定直接回答") return message.content or "" messages.append({ "role": "assistant", "content": message.content or "", "tool_calls": [tool_call.model_dump() for tool_call in message.tool_calls], }) for tool_call in message.tool_calls: fn_name = tool_call.function.name args = json.loads(tool_call.function.arguments or "{}") print(f"[第{step+1}轮] 调用工具: {fn_name}({args})") result = TOOL_MAP[fn_name](**args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) return "达到最大工具调用轮次,建议把问题拆得更小再试。" if __name__ == "__main__": print(run_agent("现在是几点?帮我算一下23*17+6,再搜索一下关于人工智能的文章。"))重点看消息结构:模型返回的工具调用结果,必须用tool角色消息且带tool_call_id回传。这个ID是对应关系,搞错了模型就无法关联结果。SDK内部会校验,但你自己手写时要保持清醒:每一轮都像一次小型的“需求-执行-汇报”会议。
max_steps就是最大轮次。我见过太多人第一次跑Agent时忘记设这个上限,结果模型反复调用失败工具,几十分钟烧掉几百次请求。这个参数后面会展开讲。
3.4 真实运行效果:一段对话如何被一步步完成
上面那段代码运行后,你会看到类似这样的日志输出:
[第1轮] 调用工具: get_current_time({'timezone_offset': 8}) [第2轮] 调用工具: calculate({'expression': '23*17+6'}) [第3轮] 调用工具: search_articles({'keyword': '人工智能'}) [第4轮] 模型决定直接回答这不是说模型“只会按顺序调用”。有些模型会把多个工具调用放在同一轮返回,比如同时给我当前时间和计算结果。具体行为取决于模型本身以及你的描述,但它们的共同点是:模型把一项复杂任务拆成了多个可执行动作,每个动作都有明确输入输出。
最后模型给出的回答大概是:
当前时间是2025-01-15 14:30:00(东八区)。计算结果是397。与“人工智能”相关的文章有:《AI Agent入门指南》《大模型函数调用实践》《工具调用与检索增强对比》。注意,模型没有自己搜索资料,它只是把你提供的工具结果重新组织成了人类语言。Agent的“智能”是在这个组织过程中体现的——它知道哪些结果该保留、哪些该省略,也知道如何回应多个子问题。
3.5 把模拟检索替换成真实API的两种思路
本地模拟搜索只是为了跑通流程。实际项目中,你需要把search_articles替换成真实接口。替换方式无非两种:
第一种是调用现成的搜索或新闻API。代码骨架长这样:
import requests def search_articles(keyword: str) -> dict: url = "https://your-api.example.com/search" # 换成实际接口 params = {"q": keyword, "limit": 5} headers = {"Authorization": "Bearer " + os.environ.get("SEARCH_API_KEY", "")} resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json()第二种是自己写一个爬虫或数据库查询函数,封装到同样的签名里。关键是:无论真实逻辑多复杂,你的Agent主循环只关心函数的入参和返回值。这也是Function Calling设计的精髓——工具内部随便折腾,对外接口必须简单清晰。
我建议初学者先用模拟工具跑通全流程,再逐步替换真实API。这样你能把“Agent循环”和“外部服务”分开排查,问题定位时少很多干扰。
4. 实测中一定会遇到的5个坑:从对话失控到上下文爆炸
Demo能跑通只是开始。我把自己实际踩过的坑整理成下面五个,每一个都附上现象和修复思路,希望对你有帮助。
4.1 坑位一:Agent陷入“调用-失败-再调用”死循环
现象:日志里反复打印同一个工具调用,比如每隔几秒就看到search_articles({'keyword': 'xxx'}),但工具返回结果后模型还是继续调用同一个工具,永远给不出最终答案。
我先说排查链路。第一步看日志里工具返回值是否正常,如果返回的是空列表或错误信息,模型可能误以为“还没获取到足够信息”而继续调用;第二步看是不是工具参数每次都一样,相同输入、相同输出,模型却仍然重复调用,这通常是描述里没写清“搜索完成即可回答”;第三步检查是不是没设轮次上限,模型在无限循环中不断耗Token。
修复方法有三个:一是给run_agent的max_steps设硬上限,这个必须做;二是把工具的 description 写得更有引导性,比如“只做一次检索,拿到结果后直接回答,不要再重复检索”;三是在工具结果里明确加一个完成标记,比如"finished": true,配合系统提示词要求模型看到标记后停止调用。
4.2 坑位二:工具返回太长,直接击穿上下文窗口
现象:Agent调了两三次工具后,突然报错context length exceeded,或者运行越来越慢、费用越来越高。
原因很直接:每次工具返回结果都会作为新消息加入messages,如果某个工具返回一整个文档或一长串数据库记录,几轮下来上下文就爆了。
我的处理思路是控制输入而非加钱换大窗口。工具函数在返回前先做裁剪,比如只返回前N条结果、截断过长的字段;同时把历史消息做轻量压缩。最简单的压缩方式是:每轮对话结束后,让模型把这轮内容总结成几句摘要,后续只传摘要而非全部历史。这就相当于给Agent配了一个“随身笔记本”,只记要点,不记流水账。
4.3 坑位三:模型“一本正经地调用错误参数”
现象:模型调用calculate时,把参数expression传成“二十三乘以十七加六”而不是"23*17+6";或者调用search_articles时把参数传成用户问题里的冗余前缀。
这个问题的根源在Schema描述不够明确。模型没有数学常识,它只根据你的描述推断参数格式。修复方式是:
- 在工具 description 里写明参数格式,甚至给出例子,比如“expression 是一个合法的数学表达式字符串,例如 '23*17+6',不要包含中文”。
- 在代码入口做参数校验,不符合格式就返回一个“参数格式错误”的工具消息,模型看到后会自己纠正。
- 必要时在系统提示词里强调“工具参数必须严格遵循Schema定义”。
这类问题不算模型“傻”,而是你没给它足够好的说明书。写工具Schema的仔细程度,直接决定Agent的表现稳定度。
4.4 坑位四:外部服务一抖动,Agent就彻底躺平
现象:某个外部API偶尔超时或返回500,Agent直接把错误抛给用户,甚至自行编造一个结果。
我见过好几次:工具函数内部没做异常处理,请求失败时抛了异常,Agent循环直接崩了。修复的关键是在工具函数内部捕获所有异常,并返回结构化错误信息,而不是让异常穿透到主循环。
比如:
def search_articles(keyword: str) -> dict: try: # 真实请求 ... return data except Exception as e: return {"error": f"搜索服务暂时不可用:{str(e)}", "keyword": keyword}这样模型收到的是“工具失败”的信息,它可以决定换个关键词重试,或者诚实告诉用户服务不可用。你要相信模型在收到准确状态信息时的判断力,它会比你写一大堆if-else灵活得多。
4.5 坑位五:把Agent的权限给得太大的安全风险
现象:为了让Agent“更强大”,你给它的工具函数绑定到了系统Shell、文件系统,甚至支付接口。然后某次用户输入了一个恶意构造的请求,Agent调用了危险操作。
安全边界必须从一开始就绷紧。我的原则是:
- 工具只能做只读或低风险操作,变更类操作必须有额外确认步骤。
- 绝不把所有工具一股脑发给模型。先按场景分组,比如“查询工具组”“写入工具组”,默认只激活需要的组。
- 工具函数内部要实现输入校验和操作白名单。
- 对支付、删除、修改权限类动作,单独加一个人工审批入口。
Agent的“自主性”是双刃剑。你想要的自主是让它在规则允许的范围内尽量高效,而不是让它变成一把失控的剑。凡是模型不能自主决定后果的操作,都不该通过Function Calling暴露给它。
5. 从Demo到真正落地:记忆、规划与评估
5.1 短期记忆与长期记忆:让Agent记住上个月聊过什么
我前面提到,模型本身无记忆,所谓记忆是把历史塞进请求。这种最简单的短期记忆只适合单次对话。如果你想跨会话记住用户偏好,比如“用户更喜欢简洁回答”“用户上次问的项目进展到哪一步”,就需要长期记忆。
新手最容易上手的长期记忆方案是:把关键信息写成结构化摘要,存到数据库或本地文件里。每次开启新对话时,先查询与当前问题相关的摘要,注入系统提示词,再让Agent回答。比直接塞全部历史高效得多,因为摘要体积小,不太占上下文窗口。
数据量大了以后,可以引入向量检索:把历史对话切片嵌成向量,按相关性召回Top-K片段。但我不建议一上来就搞向量库,先用“摘要+关键词匹配”跑通业务,等真正遇到检索瓶颈再升级。
5.2 从“走一步看一步”到“先规划再执行”
上一节的Demo是典型的“走一步看一步”模式:模型每轮只做一个小决策,轮次多了自然能完成复杂任务。这种模式简单灵活,但容易出现“做着做着就偏了”的情况。
更稳的做法是“先规划再执行”:在正式对话前,先让模型把任务拆成步骤列表,然后再逐步执行。你可以用System Prompt引导,比如:
开始任务前,先输出你的执行计划,包含最多5个步骤。每完成一步,检查是否偏离计划。如果用户需求变化,重新输出计划。还有一种做法是两层结构:一个“规划模型”负责拆解和生成计划,另一个“执行模型”负责把计划里的单个步骤跑完。对小项目来说,一个模型加一段“先列计划再执行”的提示词就够了,没必要上复杂架构。
5.3 怎么判断Agent真的变强了:评估集与成本账单
很多人在调Agent时全凭感觉:试两句话觉得“不错”就上线了,用户一反馈不好又乱了阵脚。我强烈建议准备一个固定评估集,哪怕只有20条问题,也要覆盖正常请求、边界请求、恶意请求三种类型。
每次改动Prompt或工具逻辑后,把20条问题跑一遍,记录三类指标:
- 成功率:是否给出符合预期的回答。
- 效率:平均调用几轮工具、消耗多少Token。
- 稳定性:同一问题跑多次,答案是否一致。
没有评估集,你根本分不清一次改动是变好了还是变坏了。成本也一样重要,Agent项目最大的成本就是Token消耗,每轮工具调用都在花钱。上线前算一下单次会话平均成本,能防止月底账单吓人。
5.4 个人学习的下一步路径建议
跑通本文的Demo之后,你已经有能力看懂市面上大部分Agent项目了。我建议按这个顺序继续深入:
- 把Demo改成更贴近自己业务场景的工具组合,比如接入邮件、表格、数据库等。
- 尝试写一个带摘要记忆的版本,体验一下历史压缩对效果的提升。
- 研究开源Agent编排项目,看别人如何封装规划、记忆、人机协作。
- 最后再考虑复杂框架和多Agent协作,你会发现它们都建立在同一个“决策-行动-观察”循环之上。
不用一上来就追新框架。底层逻辑想透了,工具只是外壳。
最后说一点我个人的体会。很多人觉得做Agent最难的是“模型不够聪明”,我实际用下来,最难的是“如何让一个不确定的系统稳定完成任务”。所以我在每个工具返回里都会强制带上调用来源或请求ID,排查问题时能快速定位是哪一步出了错。这个小习惯帮我省下了大量排查时间。这篇文章里的Demo只有几十行,但真正把它跑通之后,你会发现那些复杂的Agent框架,本质上都只是在这样一个循环上加了记忆、规划和防御而已。理解了这一层,AI Agent核心技术的大门就已经向你打开了。