作者:互联网 时间: 2026-08-03 11:16:09
别只会调用大模型 API:用 Python 实现一个能干活的 AI Agent的重点在于把前置条件、操作顺序和容易误判的地方分清楚。
过去两年,我们写过太多这样的代码:向大模型发送一段提示词,然后打印回答。
response = client.chat.completions.create(model="your-model",messages=[{"role":"user","content":"帮我分析这个项目"}],)print(response.choices[0].message.content)这段代码能聊天,却不能真正分析项目,因为模型既看不到项目文件,也不能执行任何操作。它只能根据训练数据和提示词猜测。
AI Agent 的关键变化,是给大模型增加一个可控的“行动层”:模型可以判断当前需要什么信息,选择合适的工具,读取执行结果,再决定下一步做什么。
例如,当用户提出:
一个 Agent 可能按下面的顺序工作:
判断需要读取文件;调用read_file;获取文件内容并进行总结;判断还需要当前时间;调用 get_current_time;综合两次工具结果,生成最终回答。这就是 Agent 最核心的闭环:
用户目标 -> 模型决策 -> 调用工具 -> 观察结果 -> 再次决策 -> 最终回答 一、先设计两个安全工具为了让示例容易运行,我们只提供两个工具:读取指定目录内的文本文件,以及获取当前时间。项目结构如下:```textmini-agent/├── agent.py└── workspace/└── README.md先实现工具函数:
from datetime import datetimefrom pathlib import Pathfrom zoneinfo import ZoneInfoWORKSPACE = Path(__file__).parent.joinpath("workspace").resolve()defread_file(path:str)->str:"""读取工作目录内的 UTF-8 文本文件。"""target = WORKSPACE.joinpath(path).resolve()# 防止 ../../ 等路径穿越访问工作目录之外的文件if target != WORKSPACE and WORKSPACE notin target.parents:return"错误:只能读取 workspace 目录内的文件"ifnot target.is_file():returnf"错误:文件不存在:{path}"if target.stat().st_size >100_000:return"错误:文件超过 100 KB,拒绝读取"try:return target.read_text(encoding="utf-8")except UnicodeDecodeError:return"错误:当前示例只支持 UTF-8 文本文件"defget_current_time(timezone:str="Asia/Shanghai")->str:"""返回指定 IANA 时区的当前时间。"""try:now = datetime.now(ZoneInfo(timezone))except Exception:returnf"错误:无效时区:{timezone}"return now.isoformat(timespec="seconds")这里有一个很重要的细节:不要把整个文件系统直接开放给 Agent。
工具参数来自模型,而模型可能受到错误提示词或 Prompt Injection 的影响。因此,工具本身必须检查路径、文件大小和数据类型。安全边界应该写在工具代码里,不能只靠一句“请勿读取敏感文件”的提示词。
大模型不会自动知道 Python 函数的存在,我们需要用 JSON Schema 描述工具名称、用途和参数。
TOOLS =[{"type":"function","function":{"name":"read_file","description":"读取 workspace 目录内的 UTF-8 文本文件","parameters":{"type":"object","properties":{"path":{"type":"string","description":"相对于 workspace 的文件路径",}},"required":["path"],"additionalProperties":False,},},},{"type":"function","function":{"name":"get_current_time","description":"获取指定 IANA 时区的当前时间","parameters":{"type":"object","properties":{"timezone":{"type":"string","description":"例如 Asia/Shanghai 或 UTC",}},"required":[],"additionalProperties":False,},},},]描述应当短而准确。如果多个工具的描述含糊或相互重叠,模型就更容易选错工具。
下面使用兼容 Chat Completions 与 Function Calling 的 HTTP 接口。通过环境变量可以更换模型服务地址,不需要把密钥写进代码。
先安装依赖:
pip install requests配置环境变量:
# Linux / macOSexportLLM_API_KEY="你的密钥"exportLLM_BASE_URL="https://你的服务地址/v1"exportLLM_MODEL="支持工具调用的模型名称"Windows PowerShell:
$env:LLM_API_KEY="你的密钥"$env:LLM_BASE_URL="https://你的服务地址/v1"$env:LLM_MODEL="支持工具调用的模型名称"然后在 agent.py 中加入 Agent 循环:
import jsonimport osfrom typing import Anyimport requestsAPI_KEY = os.environ["LLM_API_KEY"]BASE_URL = os.environ["LLM_BASE_URL"].rstrip("/")MODEL = os.environ["LLM_MODEL"]TOOL_HANDLERS ={"read_file": read_file,"get_current_time": get_current_time,}defcall_model(messages:list[dict[str, Any]])->dict[str, Any]:response = requests.post(f"{BASE_URL}/chat/completions",headers={"Authorization":f"Bearer {API_KEY}","Content-Type":"application/json",},json={"model": MODEL,"messages": messages,"tools": TOOLS,"tool_choice":"auto","temperature":0,},timeout=60,)response.raise_for_status()return response.json()["choices"][0]["message"]defexecute_tool(name:str, arguments:str)->str:handler = TOOL_HANDLERS.get(name)if handler isNone:returnf"错误:未知工具 {name}"try:kwargs = json.loads(arguments or"{}")ifnotisinstance(kwargs,dict):return"错误:工具参数必须是 JSON 对象"returnstr(handler(**kwargs))except json.JSONDecodeError:return"错误:工具参数不是合法 JSON"except TypeError as exc:returnf"错误:工具参数不正确:{exc}"except Exception as exc:returnf"错误:工具执行失败:{exc}"defrun_agent(user_input:str, max_steps:int=8)->str:messages:list[dict[str, Any]]=[{"role":"system","content":("你是一个谨慎的开发助手。根据任务选择工具;""不要猜测工具结果;完成目标后直接给出结论。"),},{"role":"user","content": user_input},]for step inrange(1, max_steps +1):message = call_model(messages)messages.append(message)tool_calls = message.get("tool_calls")or[]ifnot tool_calls:return message.get("content")or"模型没有返回内容"for tool_call in tool_calls:function = tool_call["function"]result = execute_tool(function["name"], function.get("arguments","{}"))print(f"[步骤 {step}] 调用 {function['name']} -> {result[:100]}")messages.append({"role":"tool","tool_call_id": tool_call["id"],"content": result,})returnf"任务超过最大执行步数 {max_steps},已停止"if __name__ =="__main__":question =input("请输入任务:")print("nAgent 回答:")print(run_agent(question))运行程序:
python agent.py输入任务:
读取 README.md,总结这个项目的用途,然后告诉我上海当前时间。一次典型的执行日志可能是:
[步骤 1] 调用 read_file -> 这是一个用于演示工具调用的最小 AI Agent 项目……[步骤 2] 调用 get_current_time -> 2026-07-27T15:30:18+08:00模型最后会基于真实文件内容和工具返回值组织答案,而不是凭空猜测。这也是 Agent 与普通对话接口最本质的区别。
判断一个程序是不是 Agent,不在于它使用了多少框架,而在于它是否形成了自主决策闭环。
在这个示例中:
目标:来自用户输入;决策者:大模型判断是否需要工具以及调用哪个工具;行动:Python 函数访问外部环境;观察:工具结果以tool 消息返回给模型;循环:模型根据新信息继续决策,直到给出答案。许多 Agent 框架所做的事情,本质上也是管理这套循环,并在此基础上加入状态持久化、任务规划、重试、并发和可观测性。

图 1:决策、编排和执行三层分离,工具层负责守住真正的权限边界。
模型可能反复调用同一个工具。因此必须设置 max_steps,生产环境还应记录相同工具和参数的重复次数。
系统提示词不是权限系统。文件访问范围、数据库权限、命令白名单和网络域名限制,都必须由程序强制执行。
如果直接把几十万行日志塞回上下文,不仅成本高,还会稀释真正有用的信息。应该在工具层分页、过滤或截断。
这是很多演示项目最危险的设计。删除文件、安装软件、发送消息等高风险操作,至少需要白名单、沙箱以及人工确认。
Agent 的错误可能来自模型选错工具、参数错误、工具异常或上下文污染。生产系统需要保存每一步调用耗时、参数摘要、结果状态和 Token 消耗。
本文把工具直接写在 Python 程序里,优点是容易理解,缺点是工具和 Agent 紧密耦合。
MCP(Model Context Protocol)试图为模型连接外部工具和数据源提供统一协议。你可以把文件系统、数据库、浏览器或内部平台封装成 MCP Server,让不同的 Agent 客户端用较一致的方式发现和调用它们。
可以把两者简单理解为:
Function Calling:模型如何表达“我要调用这个工具”MCP:客户端如何发现、连接和使用外部工具服务
图 2:Function Calling 描述调用意图,MCP 解决外部工具服务的发现与连接。
MCP 不会自动解决权限、安全和结果可信度问题。即使接入 MCP,服务端仍然需要进行参数验证和权限控制。
这个最小 Agent 还可以沿着四个方向继续扩展:
接入 RAG:让 Agent 检索企业文档或项目知识库;增加记忆:保存跨会话的用户偏好和任务状态;接入 MCP:把本地函数改造成可复用的工具服务;加入评测:准备固定任务集,统计成功率、调用次数和成本;人工审批:执行写文件、发消息等操作前请求确认;多 Agent 协作:让规划、编码和审查角色各自负责不同阶段。不过,多 Agent 并不一定比单 Agent 更好。角色越多,调用成本、状态同步和故障定位也越复杂。对多数业务来说,先把单 Agent 的工具、权限和评测做好,通常比急着搭建“AI 团队”更重要。
一个最小可用的 AI Agent,只需要三个核心组件:
一个支持工具调用的大模型;一组边界清晰、经过验证的工具;一个不断执行“决策—行动—观察”的循环。真正困难的部分不是让模型调用函数,而是确保它调用正确的函数、只能访问被授权的数据,并且出错时能够停止和追踪。
当我们开始讨论权限、沙箱、评测、可观测性和人工审批时,AI Agent 才真正从有趣的 Demo 走向可以交付的软件系统。
大语言模型MCP