乐于分享
好东西不私藏

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

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

点击蓝字

关注我们

第一期和第二期我们详细地了解了AI Agent CLI的整体架构、项目初始化、核心模块设计与完整代码。今天我们将继续探讨它的运行测试、进阶拓展与常见问题。

六、运行与测试

6.1 启动交互模式

# Windows PowerShell — Key 已在 .env 中时可省略$env:OPENAI_API_KEY="your-key"python agent_cli.py -v

预期交互:

在 Windows 上,模型应倾向使用 dir *.py 而非 ls(System Prompt 第 5 条 + GBK 解码共同保证可用性)。

6.2 单次提问模式

适合脚本调用或 CI 集成:

python agent_cli.py "读取 README.md 的前 3 段并总结"python agent_cli.py --max-steps 25"分析 tests 目录结构并给出测试建议"

单次模式下回答同样经 Markdown() 渲染;若管道重定向到文件,Rich 会输出 ANSI 码,可用 python agent_cli.py "..." > out.txt 或后续加 --no-markdown 开关(进阶扩展)。

6.3 建议的手动验收用例

6.4 单元测试

当前仓库 tests/test_agent.py:

from agent.tools.registry import ToolRegistryfrom agent.tools.shell_tools import register_shell_tools, run_shelldef test_run_shell():    reg = ToolRegistry()    register_shell_tools(reg)    out = run_shell("echo hello")    assert "hello" in out

运行:

pytest tests/ -v

工具 handler 是纯函数,最适合单测。测完整 Agent 循环需要 mock LLM,MVP 阶段手动验收即可。

建议补充的测试(尚未入库,可自行添加):

def test_run_shell_timeout():    out = run_shell("sleep 99", timeout=1)    assert "超时" in out

在 Windows 上可把 sleep 99 换成 timeout /t 99 /nobreak 或 ping -n 99 127.0.0.1 > nul。

6.5 调试技巧

●开 -v:Rich 彩色看清每一步调了什么工具、返回什么。

●打印 messages:在 core.py 的循环里临时 import json; print(json.dumps(self.messages, ensure_ascii=False, indent=2)),对照 API 文档检查消息格式。

●API 报错 tool_call_id not found:几乎一定是 assistant 消息的 tool_calls 没正确写回 messages。

●模型从不调工具:检查 tools 是否传入、tool_choice 是否为 "auto"、system prompt 是否足够强调「必须用工具」。

●Windows 乱码:确认 shell_tools.py 使用 _decode_output,不要用固定 UTF-8 的 text=True。

●Markdown 显示异常:模型若返回非 Markdown 纯文本,Rich 也会正常显示;若代码块未闭合,渲染可能略丑,可 prompt 要求「代码用 markdown 代码块包裹」。

七、进阶扩展

按优先级排列,每一步都不破坏核心架构,只是在现有模块上「加一层」。

7.1 增加工具

新增工具的标准流程:

1、在 agent/tools/ 下写 handler 函数

2、写 register_xxx_tools(registry) 注册 schema

3、在 Agent.__init__ 里调用 register

示例 - 代码库搜索:

# agent/tools/search_tools.pyimport subprocessdef grep_repo(pattern: str, path: str = ".") -> str:    proc = subprocess.run(        ["rg", "-n", pattern, path],        capture_output=True, text=True, timeout=30,    )    return proc.stdoutor proc.stderror"(无匹配)"def register_search_tools(registry):    registry.register(        name="grep_repo",        description="在代码库中按正则搜索文本",        parameters={            "type": "object",            "properties": {                "pattern": {"type": "string", "description": "正则表达式"},                "path": {"type": "string", "description": "搜索目录,默认当前目录"},            },            "required": ["pattern"],        },        handler=grep_repo,    )

用 Python 实现的工具(glob、pathlib)比让模型写 shell 更跨平台,推荐优先封装。

7.2 工作目录沙箱

Agent 能跑 shell、能写文件,默认等于给了模型本机权限。最小防护是限制路径:

import osfrom pathlib import PathALLOWED_ROOT = Path(os.getenv("AGENT_WORKSPACE", ".")).resolve()def safe_path(path: str) -> Path:    p = Path(path).expanduser().resolve()    ifnot str(p).startswith(str(ALLOWED_ROOT)):        raise PermissionError(f"路径越界: {p},允许范围: {ALLOWED_ROOT}")    return p

在 read_file / write_file 开头调用 safe_path(path)。Shell 侧维护命令黑名单:rm -rf /、format、shutdown、mkfs 等一律拒绝。

7.3 危险操作人工确认

写入和执行类工具执行前询问用户:

⚠️  即将执行 write_file(path="config.py", content="...")   是否继续?[y/n]:

实现方式:给 ToolRegistry.register 加 permission 字段(READ / WRITE / EXECUTE),execute 里对 WRITE/EXECUTE 调 input() 或 Confirm.ask()(Rich)确认。拒绝时返回 "用户拒绝执行" 字符串,模型会收到并调整策略。

7.4 流式输出

最终回答用 stream=True 逐 token 打印,体验更接近 ChatGPT:

stream = client.chat.completions.create(..., stream=True)for chunk in stream:    if chunk.choices[0].delta.content:        print(chunk.choices[0].delta.content, end="", flush=True)

注意:流式模式下 tool_calls 是分块到达的,需要累积 name 和 arguments 字符串后再 parse。工具调用阶段建议仍用非流式,或参考 OpenAI 文档拼装 delta。流式与 Markdown() 冲突时,可仅对最终一轮非 tool 回复流式打印纯文本。

7.5 对话持久化与上下文裁剪

REPL 多轮对话会让 messages 无限增长。两个策略:

●Sliding window:保留 system + 最近 N 轮 user/assistant。

●Tool 结果压缩:久远的 role: tool 消息只留前 200 字符。

def trim_messages(messages, max_turns=10):    system = messages[0]    rest = messages[1:]    return [system] + rest[-(max_turns * 4):]

7.6 多 Agent 协作

单个 Agent 类足够复用,换 system prompt 和工具集即可扮演不同角色:

●Planner Agent → 只输出步骤计划,不给工具

●Worker Agent → 完整工具集,执行子任务

●Critic Agent → 只读工具,审查 Worker 产出

在 CLI 层写 Orchestrator 调度:Planner 输出 plan → 拆成子任务 → Worker 逐个执行 → Critic 检查。每个 Agent 仍是同一个类,不需要新框架。

7.7 接入 MCP(Model Context Protocol)

MCP 是标准化的外部工具协议,生态里有 filesystem、git、database 等 Server。接入思路:

●用 mcp Python SDK 以 stdio 连接 MCP Server

●拉取 Server 暴露的工具列表

●动态转成 OpenAI function schema,注册进 ToolRegistry

●调用时转发给 MCP Server,结果字符串化后返回

这样你的 Agent CLI 可以复用 Cursor、Claude Desktop 同款工具生态,而不必每个集成自己写。

7.8 LLM 容错

生产环境建议给 LLMClient 加重试:

for attempt in range(3):    try:        return self.client.chat.completions.create(**kwargs)    except RateLimitError:        time.sleep(2 ** attempt)

429、5xx、网络抖动都应重试;401/404 不应重试。还可配置 OPENAI_FALLBACK_MODELS,主模型挂掉时依次尝试备用模型。

7.9 Rich 进阶

当前 MVP 已用 Rich 做基础美化,还可按需扩展:

Rich 与核心 Agent 逻辑解耦:只改 agent_cli.py 和 verbose 打印即可,不必动 ReAct 循环。

7.10 跨平台 list_dir 工具

即便有 OS 提示,模型仍可能误用 ls。可新增纯 Python 工具,彻底避免 shell 差异:

def list_dir(path: str = ".") -> str:    p = Path(path).expanduser().resolve()    ifnot p.is_dir():        return f"不是目录: {p}"    entries = sorted(p.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))    lines = [f"{'[DIR]'if e.is_dir() else'[FILE]'}{e.name}"for e in entries[:200]]    return"\n".join(lines) or"(空目录)"

在 System Prompt 中加一条:「列目录优先用 list_dir,不要 run_shell」。

八、常见问题

Q1: 模型不调用工具,只瞎编答案?

原因:模型认为凭「知识」就能答,或 prompt 未强调必须用工具。

排查:

●确认 tools=self.registry.schemas 确实传入了 API

●System prompt 写「需要查看文件时必须 read_file,不要猜测」

●换更强的模型(gpt-4o、deepseek-chat / deepseek-v4 对 tool calling 支持更好)

●用户问题要具体:「分析 core.py」比「这个项目怎么样」更容易触发工具

Q2: 工具调用死循环?

现象:同一命令反复失败,模型不停止。

对策:

●max_steps 默认 15,硬性截断

●Prompt 写:「同一操作失败两次则停止并说明原因」

●进阶:在 registry 里记录已执行命令 hash,重复则直接返回「请勿重复调用」

Q3: Context 超长 / Token 爆掉?

●read_file 截断大文件(默认 8000 字符)

●run_shell 截断输出(默认 12000 字符)

●REPL 多轮后做 sliding window 或压缩旧 tool 结果

●选用 context 更大的模型,或把「读全文件再总结」拆成「分段读取」

Q4: Windows 与 Linux 命令差异?

模型训练数据偏 Linux,在 Windows 上可能写 ls、find 而失败。

解法(当前 MVP 已部分实现):

●System prompt 通过 sys.platform 自动注明 OS 和 shell 类型(见 prompts.py)

●run_shell 对 Windows 输出做 GBK/cp936 解码(见 shell_tools._decode_output)

●封装跨平台工具:用 Python glob.glob、pathlib.iterdir 代替 shell 列目录(见 7.10)

●提供 list_dir、grep 等纯 Python 工具,减少对 shell 的依赖

Q5: API 报错 Invalid parameter: messages?

多半是 messages 格式不对。常见错误:

●assistant 有 tool_calls 但后续缺少对应 tool 消息

●tool 消息的 tool_call_id 与 assistant 里的 id 不匹配

●把 OpenAI 对象直接 append 进 list 而非 dict

用 -v + 打印 messages 对照 OpenAI 文档排查。

Q6: 和 LangChain / AutoGPT 比,什么时候该换框架?

核心建议:先按本文跑通 300 行 MVP,理解消息流和 ReAct 环;真遇到框架能省大量时间的场景再引入,避免一开始就被抽象层淹没。

Q7: Windows 上 run_shell 输出乱码?

现象:dir、中文文件名等返回 `` 或不可读字符。

原因:cmd 默认代码页多为 GBK,旧版用 encoding="utf-8" 解 subprocess 会失败。

解决:使用当前 shell_tools.py 的 bytes + _decode_output 实现;勿改回 text=True, encoding="utf-8"。

Q8: Rich Markdown 在终端里显示不对?

●终端需支持 Unicode;Windows Terminal、新版 PowerShell 一般没问题。

●代码块需模型输出标准  ```  围栏;否则按普通段落渲染。

●CI/日志收集可暂时不用 Markdown,直接 print(answer)。

●需要纯文本管道输出时,增加 --plain 开关(见 7.9)。

E n d

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

相关学习资料