200行Python代码,让程序自己会干活——手写一个AI Agent
2026 年了,光会调 API 已经不够用。这篇文章带你手写一个真正能"自主决策、调用工具、完成任务"的 AI Agent,代码不超过 200 行。
一、什么是 AI Agent?
先说清楚概念。
你平时用大模型,大概率是这个流程:发一条消息 → 模型回一条消息 → 结束。这叫"单轮对话",模型就是个高级问答机。
但 Agent 不一样。Agent 的核心是 “自主循环”:模型不只是回答问题,而是自己判断接下来该干什么——要不要查资料?要不要算个数?要不要读写文件?——然后动手去干,干完再根据结果决定下一步。
打个比方:
- 普通 LLM 像一个图书馆管理员,你问什么他答什么
- Agent 像一个实习生,你跟他说"帮我整理这周的日报",他会自己去翻文件、读内容、写总结、保存结果
Agent 跟 RAG(检索增强生成)也不是一回事。RAG 只管"查了再说",Agent 是"边想边干"。
二、核心原理:三步循环
任何一个 Agent,底层都是同一个循环:
用户指令 → LLM 推理 → 需要工具? → 是 → 调用工具 → 拿到结果 → LLM 推理 → … → 不需要了 → 返回最终结果
翻译成代码就是三个核心组件:
下面我们一步步搭。
三、环境准备
先确认 Python 版本(3.10+ 就行):
python –version
需要装两个核心库:
pip install openai httpx
这里用 OpenAI 兼容接口(DeepSeek 也能用,接口完全兼容,换 key 和 base_url 就行),方便你随时切换模型。
目录结构:
agent_demo/
├── agent.py # 主程序
├── tools.py # 工具定义
└── .env # API Key(不要提交到 git)
.env 文件内容:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com/v1
MODEL_NAME=deepseek-chat
四、第一步:定义工具箱
Agent 能干什么,取决于你给它什么工具。我们先定义三个实用的:
# tools.py
import json
import os
from datetime import datetime
def get_current_time() –> str:
"""获取当前日期和时间"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def read_file(filepath: str) –> str:
"""读取文件内容,传入文件路径"""
if not os.path.exists(filepath):
return f"文件不存在: {filepath}"
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# 太长就截断,避免撑爆上下文
if len(content) > 5000:
return content[:5000] + "\\n…(内容过长,已截断)"
return content
except Exception as e:
return f"读取失败: {str(e)}"
def write_file(filepath: str, content: str) –> str:
"""写入文件,传入文件路径和内容"""
try:
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return f"写入成功: {filepath}({len(content)} 字符)"
except Exception as e:
return f"写入失败: {str(e)}"
def run_command(command: str) –> str:
"""执行系统命令并返回结果(仅限安全命令)"""
import subprocess
# 安全白名单:只允许这些前缀的命令
allowed = ["ls", "cat", "echo", "wc", "du", "find", "grep", "head", "tail", "date"]
cmd_parts = command.strip().split()
if not cmd_parts or cmd_parts[0] not in allowed:
return f"不允许执行该命令: {cmd_parts[0] if cmd_parts else '空命令'}(白名单: {allowed})"
try:
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
output = result.stdout or result.stderr
if len(output) > 3000:
output = output[:3000] + "\\n…(输出过长,已截断)"
return output
except subprocess.TimeoutExpired:
return "命令执行超时"
except Exception as e:
return f"命令执行错误: {str(e)}"
# 工具注册表:把函数名映射到函数对象
TOOLS = {
"get_current_time": get_current_time,
"read_file": read_file,
"write_file": write_file,
"run_command": run_command,
}
关键设计点:
- 每个函数都写了 docstring,这很重要——Agent 就是靠读函数描述来决定用哪个工具的
- run_command 有安全白名单,别让 Agent 在你机器上 rm -rf /
- 文件读写都加了长度限制,防止单次工具调用撑爆 LLM 的上下文窗口
五、第二步:定义工具描述(Function Calling 格式)
LLM 不认识 Python 函数,你得用标准的 Function Calling 格式告诉它"我能用哪些工具、每个工具有什么参数":
# agent.py(接上面)
TOOL_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "获取当前日期和时间,不需要参数",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取指定文件的内容,用于查看文件、分析数据",
"parameters": {
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "要读取的文件路径"
}
},
"required": ["filepath"]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "将内容写入文件,用于保存结果、生成报告",
"parameters": {
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "目标文件路径"
},
"content": {
"type": "string",
"description": "要写入的内容"
}
},
"required": ["filepath", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "run_command",
"description": "执行安全的系统命令(仅限 ls/cat/wc/du/find/grep/head/tail/date/echo),用于获取系统信息",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的命令"
}
},
"required": ["command"]
}
}
}
]
六、第三步:调度循环——Agent 的心脏
这是整个 Agent 最核心的部分。循环逻辑很简单:
# agent.py(核心循环)
import json
import os
from openai import OpenAI
from dotenv import load_dotenv
from tools import TOOLS, TOOL_DEFINITIONS
load_dotenv()
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL")
)
MODEL = os.getenv("MODEL_NAME", "deepseek-chat")
# 系统提示词——定义 Agent 的行为模式
SYSTEM_PROMPT = """你是一个实用的 AI 助手,可以调用工具完成任务。
工作原则:
1. 接到任务后,先分析需要哪些步骤
2. 每次只执行一步——调用一个工具,观察结果,再决定下一步
3. 工具返回结果后,根据结果调整计划
4. 任务完成时,用自然语言总结你的工作成果
5. 如果工具调用失败,尝试其他方法,不要放弃
可用工具:get_current_time(获取时间)、read_file(读文件)、write_file(写文件)、run_command(执行安全命令)
"""
def run_agent(task: str, max_steps: int = 10) –> str:
"""运行 Agent,最多执行 max_steps 步"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task}
]
print(f"\\n{'='*60}")
print(f"📋 任务: {task}")
print(f"{'='*60}\\n")
for step in range(1, max_steps + 1):
print(f"— 第 {step} 步 —")
# 调用 LLM,附上工具定义
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto", # 让模型自己决定要不要调工具
temperature=0.1 # 低温度,保证决策稳定
)
msg = response.choices[0].message
# 情况1:模型觉得任务完成了,直接返回文本
if msg.content and not msg.tool_calls:
print(f"💬 Agent: {msg.content}")
messages.append({"role": "assistant", "content": msg.content})
return msg.content
# 情况2:模型要调用工具
if msg.tool_calls:
tool_call = msg.tool_calls[0]
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"🔧 调用工具: {func_name}({json.dumps(func_args, ensure_ascii=False)})")
# 执行工具
tool_func = TOOLS.get(func_name)
if not tool_func:
result = f"工具 {func_name} 不存在"
else:
try:
result = tool_func(**func_args)
except Exception as e:
result = f"工具执行出错: {str(e)}"
# 截断过长的结果
if len(str(result)) > 4000:
result = str(result)[:4000] + "\\n…(已截断)"
print(f"📤 结果: {str(result)[:200]}{'…' if len(str(result)) > 200 else ''}")
# 把工具调用和结果加入对话历史
messages.append({
"role": "assistant",
"content": None,
"tool_calls": [{
"id": tool_call.id,
"type": "function",
"function": {
"name": func_name,
"arguments": tool_call.function.arguments
}
}]
})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})
print(f"\\n⚠️ 达到最大步数限制 ({max_steps}),Agent 停止")
return "任务未完成:达到最大步数限制"
if __name__ == "__main__":
task = input("请输入任务: ")
result = run_agent(task)
print(f"\\n{'='*60}")
print(f"✅ 最终结果:\\n{result}")
代码本身不复杂,但有几个值得展开的设计决策:
1. 为什么 tool_choice="auto"?
设成 auto 之后,LLM 自己判断"这轮我是该说话还是该调工具"。如果设成 required,模型每轮必调工具,处理简单问题时反而会"为了调工具而调工具"——比如你说"你好",它可能去读一个不存在的文件再回来,多浪费一轮 token。
2. 温度设 0.1 的必要性
Agent 的每一步决策是"链式"的——这一步的错会影响后面每一步。温度高了模型会"瞎猜",选了不该选的工具或者传了错的参数,整条执行链就歪了。0.1 是实测下来兼顾稳定性和灵活性的值。
3. 为什么结果要截断?
如果不截断,Agent 读了一个 10 万行的日志文件,会把整个上下文窗口撑满。之后的推理全部失效。4000 字符是经验值——够模型判断结果有没有用,又不会炸窗口。
七、实战:让 Agent 分析一个真实项目
光说不练不行。假设你的工作目录 /home/user/projects/ 里有一堆 Python 文件,你想让 Agent 自动统计代码质量。
先造一个接近真实项目的测试环境:
mkdir -p /tmp/demo_project/src /tmp/demo_project/tests /tmp/demo_project/docs
cat > /tmp/demo_project/src/main.py << 'EOF'
import sys
from utils import parse_args, load_config
def main():
args = parse_args(sys.argv[1:])
config = load_config(args.config_path)
result = process_data(config)
print(f"处理完成,共 {result['count']} 条记录")
if __name__ == "__main__":
main()
EOF
cat > /tmp/demo_project/src/utils.py << 'EOF'
import json, os
def parse_args(args):
return {"config_path": args[0] if args else "config.json"}
def load_config(path):
with open(path) as f:
return json.load(f)
def process_data(config):
return {"count": len(config.get("items", [])), "status": "ok"}
def format_output(data):
return json.dumps(data, indent=2, ensure_ascii=False)
EOF
cat > /tmp/demo_project/tests/test_utils.py << 'EOF'
from src.utils import parse_args, load_config, format_output
def test_parse_args():
assert parse_args(["test.json"]) == {"config_path": "test.json"}
def test_parse_args_default():
assert parse_args([]) == {"config_path": "config.json"}
def test_format_output():
result = format_output({"count": 5})
assert "count" in result
assert "5" in result
EOF
echo "# Demo Project – 数据处理工具" > /tmp/demo_project/README.md
echo "python-dotenv>=1.0.0" > /tmp/demo_project/requirements.txt
现在运行 Agent,输入一个描述完整但不过于具体的任务:
python agent.py
请分析 /tmp/demo_project 项目:查看目录结构、统计 Python 代码行数、
列出所有函数、生成一份代码质量简报保存到 /tmp/demo_project/REPORT.md
Agent 的实际执行过程:
第1步 — run_command("find /tmp/demo_project -name '*.py' | xargs wc -l")
→ 统计每个文件的代码行数
第2步 — run_command("grep -n '^def ' /tmp/demo_project/src/*.py")
→ 列出所有函数定义
第3步 — read_file("/tmp/demo_project/README.md")
→ 了解项目用途
第4步 — write_file("/tmp/demo_project/REPORT.md", "项目代码质量简报…")
→ 根据前三步的信息生成报告
第5步 — 输出自然语言总结:"项目包含 2 个源文件共 N 行代码,
定义了 X 个函数,测试覆盖…"
你不需要逐条告诉它"先 ls、再 wc、再 grep"——你只需要说想要什么结果,它自己拆解步骤。
这才是 Agent 和普通脚本的本质区别。
八、进阶:给你的 Agent 加更多工具
上面只是个最小原型。实际用起来你会想加更多能力。下面给几个我实际在用、效果不错的工具:
8.1 网页搜索
def web_search(query: str) –> str:
"""搜索网页,传入搜索关键词,返回前5条结果的标题和链接"""
import httpx
# 使用 Tavily Search API(免费额度1000次/月)
# 也可以用 SearXNG 自建、SerpAPI、Bing API 等
try:
resp = httpx.post(
"https://api.tavily.com/search",
json={"query": query, "api_key": "YOUR_TAVILY_KEY", "max_results": 5},
timeout=15
)
data = resp.json()
results = []
for r in data.get("results", []):
results.append(f"- {r['title']}\\n {r['url']}\\n {r['content'][:200]}")
return "\\n".join(results) if results else "未找到结果"
except Exception as e:
return f"搜索失败: {e}"
8.2 HTTP 请求
def http_request(url: str, method: str = "GET") –> str:
"""发送 HTTP 请求,传入 URL 和请求方法,返回响应文本(前3000字符)"""
import httpx
try:
resp = httpx.request(method.upper(), url, timeout=15, follow_redirects=True)
content = resp.text[:3000]
return f"状态码: {resp.status_code}\\n内容: {content}"
except Exception as e:
return f"HTTP 请求失败: {e}"
8.3 代码执行(沙箱)
def execute_python(code: str) –> str:
"""在隔离环境中执行 Python 代码并返回结果,适合做数学计算或数据处理"""
import io, sys, traceback
old_stdout = sys.stdout
sys.stdout = buffer = io.StringIO()
try:
# 限制执行环境
exec(code, {"__builtins__": {
"print": print, "len": len, "range": range,
"int": int, "float": float, "str": str, "list": list,
"dict": dict, "set": set, "tuple": tuple, "sum": sum,
"max": max, "min": min, "sorted": sorted, "abs": abs,
"round": round, "enumerate": enumerate, "zip": zip,
"map": map, "filter": filter, "json": __import__("json")
}})
except Exception:
traceback.print_exc(file=buffer)
finally:
sys.stdout = old_stdout
output = buffer.getvalue()
return output[:3000] if output else "(无输出)"
加上这些工具之后,你的 Agent 就能搜资料、调 API、跑代码了——基本上能覆盖日常开发中 80% 的自动化场景。
九、踩坑记录
从头写 Agent 的过程中,遇到几个坑,记录一下免得你重蹈覆辙:
坑1:工具结果太长,炸了上下文
我最早没做截断,Agent 拿 cat 读了一个 5 万行的 JSON 文件,后面的推理全部抽搐。解法:每个工具返回值硬截断到 3000-4000 字符。
坑2:模型陷入"工具循环"
有一次 Agent 读文件失败了(路径写错),它没有意识到应该换个思路,而是不断换参数重试同一个工具,10 步全用完还没完成任务。解法:在系统提示词里加一句"同一工具重试 2 次还失败就换方法"。
坑3:Function Calling 格式不兼容
OpenAI 和 DeepSeek 的 Function Calling 格式 99% 兼容,但参数类型声明有细微差别。如果遇到 tool_choice 报错,换成 "auto" 就行。
坑4:温度设太高导致决策不稳定
温度 0.7 的时候,Agent 有时候会"灵机一动"跳过关键步骤直接写结论。降到 0.1 就好了。Agent 场景追求的不是创意,是可靠。
总结
200 行代码,核心就是一个循环:LLM 想 → 调工具 → 看结果 → 再想。搞懂了这个,LangChain、AutoGen、MCP 协议,都是一层皮。
别光看。打开编辑器,把上面三段的代码敲一遍。Agent 这东西,跑通一次比读十篇文章都管用。
下一篇讲怎么把这个 Agent 挂到飞书上,手机发消息就能让它干活——关注不走丢。
📌 作者:Aliaoo 🚀 专注 AI 工具实战。每篇都是亲测可跑的教程。
📬 觉得有用就点个赞,想追更就点个关注。有问题评论区见。
📌 作者:Aliaoo 🚀 专注 AI 工具实战、云部署、自动化脚本。每篇都是亲测可跑的教程。

🖥️ 需要云服务器跑项目? 👉 CSDN 开发云常年折扣,新用户首单特惠
📬 觉得有用就点个赞,想追更就点个关注——下次搜到我不靠缘分。




