点击蓝字
关注我们
1. 什么是 AI Agent CLI
1.1 从 Chat CLI 到 Agent CLI
普通 Chat CLI 的工作方式很简单:用户提问 → 模型根据训练数据和上下文直接回答 → 结束。
模型无法访问你的本地文件、无法执行命令、无法查询实时数据。你问「当前目录有几个 Python 文件」,它只能猜测或建议你手动运行命令。
Agent CLI 则多了一层「行动能力」:用户提问 → 模型决定要不要用工具 → 在本地执行工具(读文件、跑命令、查 API)→ 把执行结果作为新消息喂回模型 → 循环往复,直到模型认为可以给出最终答案。
一个典型场景:
用户: "帮我看看 backend 里有多少个 pytest 文件"Agent 第 1 轮思考: 需要枚举文件→ 调用工具 run_shell("dir backend\\test_*.py /s /b | find /c /v \"\"") # Windows→ 得到结果: 42Agent 第 2 轮: 已有足够信息,不再调工具→ 回复用户: "backend 目录下共有 42 个 pytest 文件"
这里的关键转变是:模型不再「知道一切」,而是「能去查」。Agent 的价值不在于 prompt 写得多花,而在于它拥有一个可执行的反馈环。
1.2 ReAct:Agent 的核心循环
业界最常用的模式叫 ReAct(Reason + Act):模型先推理(Reason)下一步该做什么,再行动(Act)调用工具,观察(Observe)结果后继续推理。
用伪代码表达就是:
messages = [system_prompt, user_question]whilenot done:response = LLM(messages, tools=available_tools)messages.append(assistant_message)if response.has_tool_calls:for each tool_call:result = execute(tool_call)messages.append(tool_result)# 继续循环,让模型根据结果决定下一步else:print(response.content) # 最终答案break
这个循环通常叫tool-calling loop或agent loop。OpenAI、DeepSeek、Anthropic 等主流 API 都原生支持 function/tool calling,你不需要框架帮你「编排」,几十行 Python 就能跑通。
1.3 为什么值得自己造,而不是直接用框架
建议路径:先按本文实现 MVP,亲手跑通 ReAct 循环;遇到真实需求(沙箱、审计、流式输出)再按需加模块,而不是一开始就把框架全堆进来。
1.4 本文 MVP 的能力边界
本文实现的是最小可用版本,包含:
●OpenAI 兼容 API 调用(OpenAI / DeepSeek 等)
●三个基础工具:read_file、write_file、run_shell
●ReAct 多轮推理,带 max_steps 上限
●REPL 交互 + 单次提问两种 CLI 模式
●-v verbose 模式打印工具调用过程(基于 Rich 彩色输出)
●Rich 终端体验:彩色提示符、Markdown 渲染最终回答
●跨平台 shell 适配:System Prompt 按 OS 动态提示;Windows 下子进程输出 GBK 解码
不包含(留给第 7 节进阶):流式输出、权限确认、MCP、多 Agent 协作、会话持久化等。先把核心环跑通,再迭代。
2. 整体架构
2.1 模块分层
┌─────────────────────────────────────────────────────────┐│ agent_cli.py ││ CLI 入口:argparse / Rich REPL / Markdown 输出 │└──────────────────────────┬──────────────────────────────┘│ 创建 Agent,传入 user_input + Console┌──────────────────────────▼──────────────────────────────┐│ agent/core.py ││ Agent 类:messages 历史、ReAct 循环、Rich verbose 输出 │└──────────┬───────────────────────────────┬──────────────┘│ │┌──────────▼──────────┐ ┌──────────▼──────────────┐│ agent/llm.py │ │ agent/tools/registry.py ││ OpenAI 兼容 API 封装 │ │ 工具注册、schema、执行分发 │└─────────────────────┘ └──────────┬──────────────┘│┌────────────┼────────────┐│ │ │read_file run_shell write_filefile_tools shell_tools file_tools
设计原则:单向依赖。agent_cli.py 只负责入口与展示;core.py 编排 LLM 与工具;llm.py 和 tools/ 互不感知,方便单独测试和替换。
Rich 的分工:agent_cli.py 负责 REPL 交互样式和最终回答的 Markdown 渲染;core.py 在 verbose 模式下用同一个 Console 实例打印工具调用日志,保证颜色与格式一致。
2.2 一次完整请求的消息流
假设用户问「读取 README.md 并总结」,可能发生如下消息序列:
[] role: system → SYSTEM_PROMPT(含 OS 提示)[] role: user → "读取 README.md 并总结"[] role: assistant → tool_calls: [read_file(path="README.md")][] role: tool → tool_call_id=xxx, content="(文件内容...)"[] role: assistant → content="README 主要介绍了..."
注意第 3 条:assistant 消息可以同时有 content(思考文字)和 tool_calls(工具请求)。第 4 条 tool 消息必须带 tool_call_id,与第 3 条中的 id 一一对应——这是 OpenAI API 的硬性要求,漏掉会导致下一轮请求报错。
CLI 层拿到第 5 条的 content 后,会通过 rich.markdown.Markdown 渲染(标题、列表、代码块等会高亮显示),但 messages 里存的仍是纯文本,不影响 API 调用。
2.3 推荐目录结构
ai-agent-cli/├── requirements.txt # Python 依赖├── pytest.ini # pytest 配置(pythonpath)├── .env.example # API Key 模板(默认 DeepSeek)├── .env # 本地配置(勿提交 git)├── agent_cli.py # 入口:python agent_cli.py├── agent/│ ├── core.py # Agent 主循环│ ├── llm.py # LLM 客户端封装│ ├── prompts.py # System Prompt(含 OS 动态提示)│ └── tools/│ ├── registry.py # 工具注册表│ ├── file_tools.py # 读/写文件│ └── shell_tools.py # 执行命令(含 Windows 编码处理)└── tests/└── test_agent.py
整个 MVP 约 10 个文件、300 行代码,没有多余的抽象层。
关于 __init__.py:当前实现未放置空的 agent/__init__.py,在 Python 3.3+ 下从项目根目录运行(python agent_cli.py)或通过 pytest.ini 设置 pythonpath = . 即可正常导入。
若你使用的 IDE 或打包工具要求显式包标记,可自行添加空的 agent/__init__.py 与 agent/tools/__init__.py,不影响逻辑。
3. 项目初始化
3.1 创建虚拟环境
mkdir ai-agent-cli && cd ai-agent-clipython -m venv .venv# Windows.venv\Scripts\activate# macOS / Linuxsource .venv/bin/activate
始终在项目内使用虚拟环境,避免污染系统 Python,也方便日后打包或部署。
3.2 安装依赖
requirements.txt:
openai>=1.30.0python-dotenv>=1.0.0rich>=13.0.0pytest>=8.0.0
pip install -r requirements.txt
3.3 配置环境变量
.env.example(当前仓库默认 DeepSeek 配置):
# DeepSeek(OpenAI 兼容)OPENAI_API_KEY=your-deepseek-keyOPENAI_BASE_URL=https://api.deepseek.com/v1OPENAI_MODEL=deepseek-v4-flash # 更强推理:deepseek-v4-pro
复制为 .env 并填入真实 Key:
cp .env.example .env # Linux/macOScopy .env.example .env # Windows
若使用 OpenAI 官方,只需改三行:
OPENAI_API_KEY=sk-xxxOPENAI_BASE_URL=https://api.openai.com/v1OPENAI_MODEL=gpt-4o-mini
安全提示:把 .env 加入 .gitignore,永远不要提交 API Key。
3.4 验证 API 连通性(可选)
在写 Agent 之前,可以先测 API 是否通:
from openai import OpenAIimport osfrom dotenv import load_dotenvload_dotenv()client = OpenAI(api_key=os.environ["OPENAI_API_KEY"],base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),)resp = client.chat.completions.create(model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"),messages=[{"role": "user", "content": "说 hello"}],)print(resp.choices[0].message.content)
能打印出回复,说明 Key 和网络都没问题,可以继续写 Agent。
3.5 pytest 配置(新增)
项目根目录 pytest.ini:
[pytest]pythonpath = .
这行配置让 pytest 从项目根目录解析 agent 包,无需手动 PYTHONPATH=. 或安装 editable 包。运行 pytest tests/ -v 即可。
4. 核心模块设计
4.1 LLM 客户端(agent/llm.py)
职责:封装 OpenAI Chat Completions API,统一处理 messages 和可选的 tools 参数。
关键接口:
classLLMClient:def chat(self, messages: list, tools: list | None = None) -> ChatCompletion:...
返回的 message 有两种形态:
当传入 tools 时,需同时设置 tool_choice="auto",让模型自行决定调不调工具。
若设为 "none" 则强制不调;设为 {"type":"function","function":{"name":"read_file"}} 则强制调指定工具——MVP 用 "auto" 即可。
为什么单独抽一层 LLMClient?将来换模型、加重试、加流式,只改这一文件,core.py 不动。
4.2 工具注册表(agent/tools/registry.py)
每个工具需要三样东西:
OpenAI Function Calling 的 schema 格式:
{"type": "function","function": {"name": "read_file","description": "读取本地文件内容","parameters": {"type": "object","properties": {"path": {"type": "string","description": "文件绝对或相对路径"}},"required": ["path"]}}}
registry 的核心方法:
●register(...) — 注册工具,同时存 handler 和 schema
●schemas 属性 — 返回所有 schema,传给 LLM
●execute(tool_call) — 解析 arguments JSON,调用 handler,捕获异常,始终返回字符串(错误也转成字符串喂回模型,让模型自己决定下一步)
重要约定:工具 handler 的返回值必须是 str。模型只能读文本;若 handler 返回 dict,需 json.dumps 或 str() 转成字符串。
4.3 Agent 主循环(agent/core.py)
Agent 类是整个系统的中枢,负责:
① 初始化 LLM、ToolRegistry、messages(含 system prompt)
② 接收用户输入,追加到 messages
③ 在 max_steps 限制内驱动 ReAct 循环
④ 把 assistant / tool 消息正确写回 messages
⑤(新增) 接受可选 Console 实例,verbose 模式下用 Rich 彩色打印工具调用
伪代码:
classAgent:def __init__(self, max_steps=15, verbose=False, console=None):self.console = console or Console()...def run(self, user_input: str) -> str:self.messages.append({"role": "user", "content": user_input})for step in range(self.max_steps):response = self.llm.chat(self.messages, tools=self.registry.schemas)msg = response.choices[0].messageself.messages.append(msg_to_dict(msg))ifnot msg.tool_calls:return msg.content or""for call in msg.tool_calls:result = self.registry.execute(call)self.messages.append({"role": "tool","tool_call_id": call.id,"content": result,})return"达到最大步数,任务未完成。"
几个容易踩坑的点:
●assistant 消息必须完整序列化:OpenAI 返回的是对象,直接 append(msg) 不行,要转成 dict 且保留 tool_calls 字段,否则下一轮 API 报错。
●一步可能调多个工具:模型可以在一条 assistant 消息里发多个 tool_calls(例如同时 read_file 两个文件),要逐个 execute 并逐个 append tool 消息。
●max_steps 是保险丝:防止模型陷入「读文件 → 失败 → 再读同一文件」的死循环。默认 15 步对大多数任务够用。
●verbose 输出走Rich:self.console.print(...) 带 [yellow]tool[/]、[green]result[/] 等 markup,比裸 print 更易读;CLI 层传入同一个 Console,风格统一。
4.4 System Prompt(agent/prompts.py)
System prompt 决定模型的「行为准则」。对 Agent 来说,prompt 的核心任务是:明确何时必须用工具、何时可以直接回答。
当前实现已内置 OS 感知:启动时根据 sys.platform 注入 Windows 或 Unix 的 shell 语法提示,无需手动改 prompt。
import sys_OS_HINT = ("当前环境为 Windows:run_shell 请用 cmd/PowerShell 语法(如 dir *.py、where python),不要用 ls/find/grep。"if sys.platform == "win32"else"当前环境为 Unix:run_shell 可使用 bash 常用命令(ls、find、grep 等)。")SYSTEM_PROMPT = f"""你是一个命令行 AI 助手,可以通过工具帮用户完成开发任务。规则:1. 需要查看代码或文件内容时,使用 read_file。2. 需要执行命令时,使用 run_shell。3. 修改文件前先用 read_file 确认内容。4. 回答简洁,用中文。5. {_OS_HINT}"""
Prompt 工程 Tips:
●写「不要猜测」比写「尽量准确」有效得多——模型默认倾向编造。
●写明工具优先级(先 read 再 write)可减少误操作。
●OS 提示放在 System Prompt 里比写在用户问题里更稳定——每条消息都会带上,模型不会「忘记」当前平台。
●若主要用户在 Windows,还可配合 run_shell 的 GBK 解码(见 4.5 节),避免中文命令输出乱码。
4.5 CLI 入口(agent_cli.py)
入口层做三件事:解析参数、Rich 交互、Markdown 展示结果。
●有 positional prompt → 单次提问,Markdown 渲染后退出
●无 prompt → 进入 Rich REPL(Prompt.ask 彩色 you> 提示符)
●-v → 传给 Agent,Rich 彩色打印每步工具调用和结果预览
●--max-steps → 控制 ReAct 上限
REPL 里处理 EOFError(Ctrl+D)和 KeyboardInterrupt(Ctrl+C),避免异常退出不留提示。
Rich 集成要点:
4.6 Shell 工具与 Windows 编码
(agent/tools/shell_tools.py,新增小节)
早期 MVP 用 subprocess.run(..., text=True, encoding="utf-8"),在 Windows 上跑 dir 等 cmd 命令时,输出常为 GBK/cp936,硬解 UTF-8 会产生乱码,模型读到的工具结果不可信。
当前实现改为:
① subprocess.run 不设 text=True,先拿 bytes
② _decode_output(data) 按平台尝试解码:
●oWindows:gbk → cp936 → utf-8
●oUnix:utf-8
③ 全部失败则 utf-8 + errors="replace" 兜底
这样 Agent 在 Windows 上执行 dir、where python 等命令时,工具返回的中文路径、文件名才是可读的。
System Prompt 里的 OS 提示与这一层解码配合使用:前者引导模型写对命令,后者保证输出不被乱码毁掉。
未完待续
下期我们继续探索完整代码、运行测试、进阶拓展与常见问题。
声明:本文为51Testing软件测试网 blues_C 用户投稿内容,该用户投稿时已经承诺独立承担涉及知识产权的相关法律责任,并且已经向51Testing承诺此文并无抄袭内容。发布本文的用途仅仅为学习交流,不做任何商用,未经授权请勿转载,否则作者和51Testing有权追究责任。如果您发现本公众号中有涉嫌抄袭的内容,欢迎发送邮件至:editor@51testing.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

