1. 引言
AI Agent 是当前大模型应用中最热门的方向之一。它不再局限于简单的"问答",而是能够自主规划任务、调用工具、观察结果并迭代执行,最终完成一个相对复杂的目标。
而 CLI(命令行界面)是 Agent 最自然的载体之一:它轻量、无界面依赖、易于脚本化,非常适合用来演示 Agent 的核心运行机制。本文将带你从零开始,用 Python 手写一个最小可用的 AI Agent CLI,不依赖任何 Agent 框架,只借助大模型 API 和标准库,让你彻底理解 Agent 的内部工作原理。
2. 什么是 AI Agent
在动手写代码之前,我们先明确概念。一个典型的 AI Agent 通常包含以下几个核心组件:
- 大模型(LLM):负责理解用户意图、进行推理和决策。
- 工具(Tools):Agent 可以调用的外部能力,如搜索、计算器、文件读写、执行 Shell 命令等。
- 规划(Planning):将复杂任务拆解为多个步骤。
- 记忆(Memory):保存对话历史与中间结果,供后续步骤参考。
- 循环(Loop):Agent 的核心运行机制------思考、调用工具、观察结果、再思考,直到任务完成。
本文实现的 CLI 将聚焦于"循环 + 工具调用"这一最小闭环,这也是 Agent 最本质的部分。
3. 整体架构设计
我们先设计整体流程,再逐步实现。
#mermaid-svg-WZMwhH9wM3qAIwf0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WZMwhH9wM3qAIwf0 .error-icon{fill:#552222;}#mermaid-svg-WZMwhH9wM3qAIwf0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WZMwhH9wM3qAIwf0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .marker.cross{stroke:#333333;}#mermaid-svg-WZMwhH9wM3qAIwf0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WZMwhH9wM3qAIwf0 p{margin:0;}#mermaid-svg-WZMwhH9wM3qAIwf0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster-label text{fill:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster-label span{color:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster-label span p{background-color:transparent;}#mermaid-svg-WZMwhH9wM3qAIwf0 .label text,#mermaid-svg-WZMwhH9wM3qAIwf0 span{fill:#333;color:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .node rect,#mermaid-svg-WZMwhH9wM3qAIwf0 .node circle,#mermaid-svg-WZMwhH9wM3qAIwf0 .node ellipse,#mermaid-svg-WZMwhH9wM3qAIwf0 .node polygon,#mermaid-svg-WZMwhH9wM3qAIwf0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .rough-node .label text,#mermaid-svg-WZMwhH9wM3qAIwf0 .node .label text,#mermaid-svg-WZMwhH9wM3qAIwf0 .image-shape .label,#mermaid-svg-WZMwhH9wM3qAIwf0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-WZMwhH9wM3qAIwf0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .rough-node .label,#mermaid-svg-WZMwhH9wM3qAIwf0 .node .label,#mermaid-svg-WZMwhH9wM3qAIwf0 .image-shape .label,#mermaid-svg-WZMwhH9wM3qAIwf0 .icon-shape .label{text-align:center;}#mermaid-svg-WZMwhH9wM3qAIwf0 .node.clickable{cursor:pointer;}#mermaid-svg-WZMwhH9wM3qAIwf0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .arrowheadPath{fill:#333333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WZMwhH9wM3qAIwf0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WZMwhH9wM3qAIwf0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WZMwhH9wM3qAIwf0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster text{fill:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 .cluster span{color:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WZMwhH9wM3qAIwf0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WZMwhH9wM3qAIwf0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-WZMwhH9wM3qAIwf0 .icon-shape,#mermaid-svg-WZMwhH9wM3qAIwf0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WZMwhH9wM3qAIwf0 .icon-shape p,#mermaid-svg-WZMwhH9wM3qAIwf0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WZMwhH9wM3qAIwf0 .icon-shape .label rect,#mermaid-svg-WZMwhH9wM3qAIwf0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WZMwhH9wM3qAIwf0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WZMwhH9wM3qAIwf0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WZMwhH9wM3qAIwf0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户输入任务
构造系统提示词
调用大模型
模型是否请求调用工具?
解析工具调用参数
执行本地工具函数
将结果返回给模型
输出最终回答
结束
整个 CLI 的核心就是一个 while 循环:不断把"对话历史 + 工具结果"喂给模型,直到模型不再请求调用工具为止。
4. 环境准备
本文使用 Python 3.10+,并借助 OpenAI 兼容接口调用大模型(可适配 OpenAI、DeepSeek、通义千问等任意兼容服务)。
bash
# 安装依赖
pip install openai
然后设置环境变量:
bash
export OPENAI_API_KEY="sk-xxxx"
export OPENAI_BASE_URL="https://api.openai.com/v1"
如果你使用国内模型服务,只需将 OPENAI_BASE_URL 指向对应服务的兼容地址即可。
5. 定义工具
Agent 的能力上限取决于它能调用哪些工具。我们先实现两个最基础的工具:一个执行 Shell 命令,一个做简单的文件读取。
python
# tools.py
import subprocess
import os
def run_shell(command: str) -> str:
"""执行 Shell 命令并返回输出结果。"""
try:
result = subprocess.run(
command, shell=True, capture_output=True, text=True, timeout=30
)
output = result.stdout + result.stderr
return output.strip() or "(无输出)"
except subprocess.TimeoutExpired:
return "(命令执行超时)"
except Exception as e:
return f"(执行出错: {e})"
def read_file(path: str) -> str:
"""读取指定文本文件的内容。"""
try:
if not os.path.isfile(path):
return f"(文件不存在: {path})"
with open(path, "r", encoding="utf-8") as f:
return f.read()[:3000] # 限制长度,避免上下文溢出
except Exception as e:
return f"(读取失败: {e})"
# 工具注册表:Agent 通过这个名字找到对应函数
TOOLS = {
"run_shell": {
"function": run_shell,
"description": "执行 Shell 命令,参数 command 为要执行的命令字符串。",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的 Shell 命令"}
},
"required": ["command"],
},
},
"read_file": {
"function": read_file,
"description": "读取文本文件内容,参数 path 为文件路径。",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"}
},
"required": ["path"],
},
},
}
这里的关键设计是工具注册表:每个工具包含函数本身、描述和参数 Schema。描述会随系统提示词一起发给模型,让模型知道"在什么情况下该调用哪个工具"。
6. 实现 Agent 核心循环
接下来是 Agent 的心脏------主循环。我们创建一个 agent.py,负责与大模型对话、解析工具调用、执行工具并回传结果。
python
# agent.py
import json
from openai import OpenAI
from tools import TOOLS
client = OpenAI() # 自动读取 OPENAI_API_KEY 和 OPENAI_BASE_URL
SYSTEM_PROMPT = """你是一个运行在终端里的 AI 助手,可以通过调用工具来完成任务。
你可以使用的工具如下:
{}
请根据用户需求,一步步调用工具完成任务。每次只调用一个工具,观察结果后再决定下一步。
当任务完成时,直接给出最终回答,不要再调用工具。
""".format(
"\n".join(
f"- {name}: {info['description']}" for name, info in TOOLS.items()
)
)
def run_agent(user_input: str, max_steps: int = 10) -> None:
"""Agent 主循环:思考 -> 调用工具 -> 观察 -> 再思考,直到完成。"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input},
]
for step in range(max_steps):
print(f"\n[Step {step + 1}] 调用大模型...")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=[
{
"type": "function",
"function": {
"name": name,
"description": info["description"],
"parameters": info["parameters"],
},
}
for name, info in TOOLS.items()
],
)
message = response.choices[0].message
# 模型没有请求调用工具 -> 任务完成,输出最终回答
if not message.tool_calls:
print(f"\n[完成] {message.content}")
return
# 模型请求调用工具 -> 把这条消息加入历史
messages.append(message)
# 逐个执行工具调用
for tool_call in message.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"[调用工具] {fn_name}({fn_args})")
# 从注册表找到并执行工具
if fn_name in TOOLS:
result = TOOLS[fn_name]["function"](**fn_args)
else:
result = f"(未知工具: {fn_name})"
print(f"[工具结果] {result[:200]}...")
# 把工具结果作为 role=tool 的消息回传给模型
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
)
print("\n[达到最大步数,强制结束]")
if __name__ == "__main__":
task = input("请输入任务: ")
run_agent(task)
这段代码的核心逻辑只有三步:
- 把
messages(含系统提示词、用户输入、历史工具结果)发给模型; - 判断模型返回的是最终回答 还是工具调用请求;
- 若是工具调用,执行工具并把结果以
role=tool的消息追加回历史,然后继续循环。
7. 运行与测试
保存以上两个文件后,在终端运行:
bash
python agent.py
输入一个需要多步操作的任务,例如:
请输入任务: 帮我创建一个名为 demo.txt 的文件,内容写入 "hello agent",然后读取它并告诉我内容。
你会看到 Agent 自动完成以下步骤:
text
[Step 1] 调用大模型...
[调用工具] run_shell({'command': 'echo "hello agent" > demo.txt'})
[工具结果] (无输出)...
[Step 2] 调用大模型...
[调用工具] read_file({'path': 'demo.txt'})
[工具结果] hello agent...
[Step 3] 调用大模型...
[完成] 文件 demo.txt 已创建,内容为 "hello agent"。
可以看到,Agent 自主完成了"创建文件 -> 读取文件 -> 汇总回答"的完整闭环,全程无需人工干预。
8. 进阶优化方向
当前实现是一个最小可用的 Agent,距离生产级还有不少距离。你可以从以下几个方向继续完善:
- 多工具并行调用:让模型一次请求调用多个工具,减少交互轮次。
- 记忆管理:引入滑动窗口或向量数据库,处理超长对话历史。
- 错误重试机制:工具执行失败时,自动把错误信息回传给模型让其修正。
- 任务规划:在循环前先让模型输出一份执行计划,再逐步执行。
- 流式输出 :使用
stream=True让回答逐字显示,提升交互体验。 - 安全沙箱:对 Shell 工具做白名单限制,避免危险命令。
9. 总结
本文从零实现了一个 AI Agent CLI,核心只有两个文件、一个 while 循环。你可能会惊讶于它的简洁------Agent 的本质并不神秘,就是"模型 + 工具 + 循环"三者的组合。
理解了这个最小闭环,再去学习 LangChain、AutoGPT 等框架时,你会发现它们只是在"工具管理、记忆、规划"等外围能力上做了增强,核心骨架与本文完全一致。希望这篇文章能帮你彻底打通 Agent 的实现原理。