大数跨境

亲手造一个AI Agent CLI没你想的难,用python就够了

亲手造一个AI Agent CLI没你想的难,用python就够了 51Testing软件测试网
2026-07-20
6
导读:本文从零实现一个AI Agent CLI,核心是ReAct循环与工具注册表,涵盖LLM调用、跨平台shell适配及Rich交互。

点击蓝字

关注我们

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   得到结果: 42
Agent 第 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_file                         file_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 并总结」,可能发生如下消息序列:

[1] role: system     → SYSTEM_PROMPT(含 OS 提示)[2] role: user       → "读取 README.md 并总结"[3] role: assistant  → tool_calls: [read_file(path="README.md")][4] role: tool       → tool_call_id=xxx, content="(文件内容...)"[5] 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].message            self.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 上执行 dirwhere python 等命令时,工具返回的中文路径、文件名才是可读的。


System Prompt 里的 OS 提示与这一层解码配合使用:前者引导模型写对命令,后者保证输出不被乱码毁掉。

未完待续

下期我们继续探索完整代码、运行测试、进阶拓展与常见问题。

声明:本文为51Testing软件测试网 blues_C 用户投稿内容,该用户投稿时已经承诺独立承担涉及知识产权的相关法律责任,并且已经向51Testing承诺此文并无抄袭内容。发布本文的用途仅仅为学习交流,不做任何商用,未经授权请勿转载,否则作者和51Testing有权追究责任。如果您发现本公众号中有涉嫌抄袭的内容,欢迎发送邮件至:editor@51testing.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。


图片

【声明】内容源于网络
0
0
51Testing软件测试网
博为峰51Testing软件测试网提供各种线上招聘、线上课程等网络服务,出版软件测试系列丛书及电子杂志,组织线上技术交流活动;同时还举办多种线下公益活动,如软件测试沙龙、软件测试专场招聘会等。
内容 3909
粉丝 0
51Testing软件测试网 博为峰51Testing软件测试网提供各种线上招聘、线上课程等网络服务,出版软件测试系列丛书及电子杂志,组织线上技术交流活动;同时还举办多种线下公益活动,如软件测试沙龙、软件测试专场招聘会等。
总阅读2.0k
粉丝0
内容3.9k