☰
从零构建轻量级AI Agent:Python CLI工具Agent-Reach实战指南
2026/10/9 4:03:35 网站建设 项目流程

1. 为什么我要自己搭一个 Agent-Reach

先说清楚 Agent-Reach 是什么。简单讲,它是我用 Python 写的一个命令行工具,核心目标只有一个:让 AI Agent 能够"够得着"外部世界。你本地跑一个大模型也好,接一个云端 API 也好,模型本身是封闭的,它不知道今天的天气、读不到你本地的文件、也发不出消息。Agent-Reach 就是补上这一段——把模型和真实环境之间的那层"手"给接出来。

我最初动这个念头,是因为用 Codex CLI 和 LM Studio 这类工具的时候,反复遇到同一个问题:模型能推理,但一到"帮我读一下这个目录里的配置""把这个结果写到文件里""根据这个网页内容总结一下"就卡住了。要么是工具链没接上,要么是权限和路径处理得一团糟。市面上的 Agent 框架不少,但要么太重,要么把简单的事情包装成一堆抽象概念,改起来反而费劲。

Agent-Reach 的定位很明确:轻量、可读、可改。整个项目用 Python 写,不依赖复杂的编排框架,核心就是"工具注册 + 意图解析 + 执行回传"这三步。你拿到源码之后,半小时内能看懂主流程,一小时能加上自己的工具。它适合几类人:想入门 AI Agent 开发但被各种框架劝退的初学者;手里有本地模型、想让它干点实事的折腾党;以及需要快速验证某个 Agent 想法、不想搭一整套基础设施的开发者。

关键词里出现的 CLI、AI Agent、Python、GitHub 这几个词,基本就是它的全部骨架。CLI 是交互形态,AI Agent 是能力定位,Python 是实现语言,GitHub 是分发和协作方式。下面我会把这四块拆开讲透,包括我踩过的坑和最后稳定下来的方案。

2. 整体架构设计与技术选型思路

2.1 为什么是 CLI 而不是 Web 或 GUI

很多人做 Agent 第一反应是搞个网页界面,聊天框一摆,看起来很像那么回事。我试过,结论是:在验证阶段,CLI 的效率碾压 GUI。原因很实际——Agent 的核心是"工具调用循环",你需要频繁看日志、改参数、重跑。GUI 里这些东西要么藏在控制台,要么被前端状态管理搅得一团乱。CLI 下所有输入输出都在一个终端里,print调试、管道重定向、脚本化批量测试全都顺手。

另一个原因是资源占用。本地跑模型本来就吃内存,再挂一个浏览器和一个前端 dev server,机器直接喘不过气。CLI 进程干净,启动快,关掉也彻底。Agent-Reach 的交互循环就是一个while True加input(),配合命令解析,简单到不能再简单,但足够用。

当然 CLI 也有代价:没有富文本展示,工具执行结果得自己格式化。我的处理是用简单的缩进和分隔线,配合颜色(colorama或者直接 ANSI 转义),可读性够用。真要展示复杂结构,就输出 JSON,让用户自己接jq。

2.2 Python 作为实现语言的取舍

选 Python 不是因为它"最适合"Agent,而是因为它的生态和上手成本。requests发请求、pathlib处理路径、subprocess调外部命令、json解析配置,这些标准库和常用包把 90% 的脏活都干了。你要接一个本地模型,openai这个包改个base_url就能指向 LM Studio 或兼容接口,省掉大量适配工作。

Python 的短板也明显:并发弱、打包分发麻烦、类型系统靠自觉。但 Agent-Reach 这种工具,瓶颈在模型推理和网络 IO,不在语言本身。真到了需要高并发的场景,我会把重活丢给外部进程,Python 只做编排。至于分发,直接给源码 +requirements.txt是最省事的,用户pip install -r就完事,比打包成二进制少一堆平台兼容问题。

提示:如果你打算把 Agent-Reach 部署到没有 Python 环境的机器上,优先考虑用venv隔离依赖,而不是全局安装。全局装一堆包,后面版本冲突能让你怀疑人生。

2.3 工具注册机制:整个项目的核心

Agent-Reach 最关键的设计是工具注册表。每个能力(读文件、写文件、发 HTTP 请求、执行 shell 命令)都是一个函数,带一段描述和参数 schema。注册表把这些函数收集起来,转成模型能理解的工具列表,模型决定调哪个、传什么参数,执行完再把结果塞回对话。

为什么用这种"函数即工具"的方式,而不是搞一套类继承体系?因为可读性。一个工具就是一个普通函数,上面挂个装饰器:

@tool( name="read_file", description="读取指定路径的文本文件内容", params={"path": {"type": "string", "description": "文件绝对或相对路径"}} ) def read_file(path: str) -> str: return Path(path).read_text(encoding="utf-8")

装饰器把函数塞进全局TOOLS字典。要加新工具,写个函数加装饰器就行,不用改任何核心代码。这种设计的好处是,你甚至可以让模型自己"提议"新工具,然后你手动实现——扩展路径非常短。

参数 schema 用 JSON Schema 的子集,够模型理解就行,不用上 Pydantic 那种重家伙。校验逻辑自己写十几行,比引入依赖更可控。

2.4 与主流 Agent 架构的对比

现在流行的 Agent 架构大致分几类:ReAct(推理-行动循环)、Plan-and-Execute(先规划再执行)、多 Agent 协作。Agent-Reach 走的是最朴素的 ReAct 路线:模型输出思考,决定调工具,拿到结果继续思考,直到给出最终答案。

我没上 Plan-and-Execute,是因为对单机小工具来说,规划层带来的收益抵不过复杂度。模型先写一个五步计划,然后执行到第三步发现前提错了,还得回头改计划,来回折腾。ReAct 的即时反馈反而更稳。多 Agent 协作就更不用说了,那是团队级项目的玩法,个人工具上纯属杀鸡用牛刀。

这个取舍背后的判断标准很简单:你的任务是否足够复杂,以至于单轮推理搞不定?如果大部分场景模型一两轮就能收敛,就别加规划层。Agent-Reach 面向的就是这类"够得着就行"的轻任务。

3. 核心模块拆解与实操要点

3.1 模型接入层:兼容 OpenAI 协议是最大公约数

Agent-Reach 的模型接入只做一件事:把对话历史和工具列表发给一个兼容 OpenAI Chat Completions 协议的接口,拿回模型的响应。为什么死磕这个协议?因为它已经成了事实标准。LM Studio 本地起服务是这个协议,绝大多数云端模型也提供兼容端点,你只要改base_url和api_key,代码一行不用动。

配置我放在一个config.json里:

{ "base_url": "http://localhost:1234/v1", "api_key": "not-needed", "model": "local-model", "temperature": 0.2, "max_tokens": 2048 }

temperature设 0.2 是有讲究的。Agent 场景要的是稳定和可预测,不是创意。温度高了,模型可能这次调read_file,下次自己编一个不存在的工具名。0.2 在保证一定灵活性的同时,把乱来的概率压到很低。max_tokens别设太大,本地模型上下文有限,留足空间给工具返回结果。

注意:用 LM Studio 起本地服务时,如果提示 "model not found",九成是模型标识符写错了。LM Studio 的模型名不是文件名,要去它的服务页面看实际暴露的id,一字不差地填进配置。这个坑我踩过,排查了半小时才发现是名字对不上。

3.2 工具执行层:安全边界必须自己划

工具执行是整个项目里最需要谨慎的部分。模型说"执行rm -rf /",你要是真执行了,那就不是 Agent 是灾难。Agent-Reach 的做法是给每个工具加白名单和路径约束。

文件类工具,我强制所有路径必须落在项目工作目录内。实现方式是把用户传入的路径resolve()之后,检查它是否以工作目录为前缀:

def _safe_path(p: str) -> Path: base = Path.cwd().resolve() target = (base / p).resolve() if not str(target).startswith(str(base)): raise ValueError("路径越界,拒绝访问") return target

shell 命令工具更狠,我直接维护一个允许的命令前缀列表,ls、cat、grep、find这些只读命令放行,rm、mv、curl一律拒绝。有人会说你这样限制太死,Agent 还能干啥?我的观点是:先保证不出事,再谈能力。真要放开,也应该由用户显式改配置,而不是默认就给。

这套约束的代价是模型有时候会"撞墙"——它想干的事被拦了。这时候工具返回一个明确的错误信息,模型会自己调整策略,比如改用read_file而不是cat。实测下来,模型对这类反馈的适应能力比想象中强。

3.3 对话循环:终止条件比循环本身更重要

Agent 的主循环逻辑不复杂:发消息、收响应、如果有工具调用就执行、把结果追加进历史、再发。真正难的是什么时候停。

我设了三重终止条件。第一,模型返回的响应里没有工具调用,只有文本,说明它认为任务完成了,循环结束。第二,达到最大轮数(我设 10 轮),防止模型陷入死循环反复调同一个工具。第三,检测重复调用——如果连续两轮调用的工具名和参数完全一样,直接中断并提示。

第二和第三条是血泪教训。早期版本没设轮数上限,有一次模型卡在"读文件-发现内容不对-再读同一个文件"的循环里,跑了二十多轮,token 烧得飞快。加上限制之后,最坏情况也可控了。

历史管理也有讲究。对话历史不能无限增长,否则上下文很快爆掉。我的策略是保留系统提示 + 最近 N 轮完整对话,更早的工具返回结果做摘要压缩。压缩逻辑很土:超过 500 字符的结果截断,保留头尾。够用,而且不会因为摘要本身引入错误。

3.4 系统提示词:决定 Agent 行为的天花板

系统提示词是很多人忽视、但实际影响最大的部分。Agent-Reach 的系统提示我改了十几版,最后稳定下来的核心是这几条:

  • 明确告诉模型它有哪些工具,以及优先用工具而不是凭记忆回答
  • 要求它在调用工具前,用一句话说明为什么调这个工具
  • 规定输出格式,最终答案要简洁,不要复述工具返回的原始内容
  • 强调遇到工具报错时,先分析原因再决定是否重试

第三条特别重要。不加约束的话,模型会把read_file返回的整段文件内容原封不动贴给用户,体验极差。加了"总结而非复述"的要求后,输出质量明显提升。

提示词里我还放了一个"能力边界"声明:告诉模型它不能访问网络(除非有对应工具)、不能执行被拒绝的命令。这样模型在规划时就不会提出做不到的方案,减少无效尝试。

4. 从零搭建 Agent-Reach 的完整实操

4.1 环境准备与依赖安装

先把地基打好。我推荐 Python 3.10 以上,因为用到了match语法和一些新的类型标注特性。3.8 也能跑,但要改几处语法。安装步骤:

# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install openai requests colorama

依赖就这三个,故意保持精简。openai负责模型通信,requests给 HTTP 工具用,colorama让终端输出有颜色。有人问为什么不用httpx或aiohttp,答案是同步够用。Agent 的瓶颈在模型推理,网络请求那点延迟不值得引入异步复杂度。

如果你要用到图像处理类的工具(比如热词里提到的cv2),再单独pip install opencv-python。但 Agent-Reach 核心不依赖它,按需装就行。

提示:国内装包慢的话,配一个镜像源能省不少时间。pip config set global.index-url指向一个可用的镜像即可,具体地址自己找当前可用的,这里不展开。

4.2 项目目录结构

我习惯把结构弄得一目了然,方便后面加东西:

agent-reach/ ├── main.py # 入口,对话循环 ├── config.json # 模型配置 ├── tools/ │ ├── __init__.py # 工具注册表 │ ├── file_ops.py # 文件读写 │ ├── shell_ops.py # 命令执行 │ └── http_ops.py # 网络请求 ├── core/ │ ├── llm.py # 模型通信 │ └── safety.py # 路径与命令校验 └── requirements.txt

tools/__init__.py里维护那个全局TOOLS字典和tool装饰器。各个*_ops.py导入装饰器,定义自己的工具。main.py导入tools包,所有工具自动注册。这种"导入即注册"的模式,加新工具时只要在tools/下新建文件并在__init__.py里 import 一下,零侵入。

4.3 工具注册表与装饰器实现

这是核心中的核心,我把完整实现贴出来:

# tools/__init__.py TOOLS = {} def tool(name, description, params): def decorator(func): TOOLS[name] = { "function": func, "schema": { "type": "function", "function": { "name": name, "description": description, "parameters": { "type": "object", "properties": params, "required": list(params.keys()) } } } } return func return decorator def get_tool_schemas(): return [t["schema"] for t in TOOLS.values()] def execute_tool(name, args): if name not in TOOLS: return f"错误:未知工具 {name}" try: return TOOLS[name]["function"](**args) except Exception as e: return f"工具执行失败:{e}"

execute_tool里的异常捕获很关键。工具执行出错不能让整个程序崩掉,要把错误信息作为结果返回给模型,让它自己判断怎么办。这个设计让 Agent 有了"容错重试"的能力。

4.4 对话主循环的完整代码

main.py的主循环,我精简后大概长这样:

import json from openai import OpenAI from tools import get_tool_schemas, execute_tool with open("config.json") as f: cfg = json.load(f) client = OpenAI(base_url=cfg["base_url"], api_key=cfg["api_key"]) messages = [{"role": "system", "content": SYSTEM_PROMPT}] MAX_ROUNDS = 10 last_call = None while True: user_input = input("\n你> ") if user_input.strip() in ("exit", "quit"): break messages.append({"role": "user", "content": user_input}) for round_idx in range(MAX_ROUNDS): resp = client.chat.completions.create( model=cfg["model"], messages=messages, tools=get_tool_schemas(), temperature=cfg["temperature"] ) msg = resp.choices[0].message messages.append(msg) if not msg.tool_calls: print(f"\nAgent> {msg.content}") break for call in msg.tool_calls: args = json.loads(call.function.arguments) signature = (call.function.name, json.dumps(args, sort_keys=True)) if signature == last_call: print("\nAgent> 检测到重复调用,已中断") break last_call = signature print(f"\n[调用] {call.function.name}({args})") result = execute_tool(call.function.name, args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result)[:2000] }) else: print("\nAgent> 达到最大轮数,已停止")

注意content那里做了 2000 字符截断,防止单个工具返回结果把上下文撑爆。last_call的重复检测放在工具执行前,避免无意义的重复执行。

4.5 一个完整的运行示例

假设我让 Agent 读一下当前目录的文件列表,然后总结。实际交互大概是这样:

你> 看看当前目录有哪些文件,帮我总结一下项目结构 [调用] list_dir({"path": "."}) [调用] read_file({"path": "requirements.txt"}) [调用] read_file({"path": "config.json"}) Agent> 当前目录是一个 Python 项目,包含入口文件 main.py、 配置文件 config.json、tools 和 core 两个包目录。 依赖只有 openai、requests、colorama 三个,配置里模型指向 本地服务。整体是一个轻量的 CLI Agent 工具。

整个过程模型自主决定调了三次工具,先列目录,再挑关键文件读,最后总结。这就是 ReAct 循环的典型表现。你会发现它没有读所有文件,而是有选择地读——这是系统提示里"优先获取关键信息"那条在起作用。

5. 常见问题排查与避坑经验

5.1 模型不调用工具,直接瞎编答案

这是新手最常遇到的问题。模型明明有工具,却凭记忆回答,甚至编造文件内容。原因通常是系统提示不够强硬,或者工具描述写得太模糊。

解决办法有三步。第一,系统提示里明确写"涉及文件、目录、网络的操作必须调用工具,禁止凭记忆回答"。第二,工具描述要具体,比如read_file的描述写成"读取指定路径的文本文件内容,返回完整文本",而不是笼统的"读文件"。第三,如果模型还是不听,把temperature再调低到 0.1。

我用本地小模型时遇到过这个,换成描述更详细的工具 schema 后明显改善。模型对工具描述的理解程度,直接决定它用不用、用得对不对。

5.2 工具参数解析失败

模型返回的arguments是 JSON 字符串,偶尔会格式错误,比如多一个逗号、少一个引号。直接json.loads会抛异常。我的处理是加一层容错:

def parse_args(raw): try: return json.loads(raw) except json.JSONDecodeError: # 尝试修复常见问题 fixed = raw.strip().rstrip(",") try: return json.loads(fixed) except Exception: return {}

返回空字典意味着工具会用默认参数执行,或者因为缺参数报错,错误信息再回传给模型。实测大部分格式问题模型下一轮能自己修正。

5.3 上下文爆炸与 token 超限

对话轮数一多,历史消息累积,很容易超过模型的上下文窗口。表现是请求报错,或者模型开始"失忆",忘记前面的内容。

我的应对策略是滑动窗口 + 结果截断。保留系统提示和最近 6 轮对话,更早的丢弃。工具返回结果统一截断到 2000 字符。这样即使跑几十轮,上下文也能控制在合理范围。代价是早期信息会丢失,但对大多数任务来说,最近几轮的信息才是决策依据。

如果你用的是长上下文模型,可以放宽这个限制。但记住,上下文越长,推理越慢,成本越高,不是越长越好。

5.4 常见问题速查表

现象可能原因解决方向
模型不调工具提示词弱、工具描述模糊强化系统提示,细化工具描述
参数解析失败模型输出 JSON 格式错误加容错解析,回传错误让模型重试
上下文超限历史消息累积过多滑动窗口,截断工具结果
重复调用死循环缺少重复检测记录上次调用签名,重复即中断
路径越界报错模型传了绝对路径提示词说明只能用相对路径
本地模型无响应服务未启动或端口错检查 base_url 和模型 id

5.5 几个我踩过的坑

第一个坑是工具返回结果太长。早期我让read_file返回完整文件内容,结果一个几百行的日志文件直接把上下文塞满,模型后面几轮全在"消化"这个文件,忘了原本的任务。后来改成超过阈值就截断,并在结果里注明"内容已截断",模型就知道信息不全,会主动再读或换策略。

第二个坑是模型对相对路径的理解。它有时候会传./config.json,有时候传config.json,还有时候传绝对路径。我的_safe_path统一处理,但提示词里也明确说了"使用相对于工作目录的路径",减少无效尝试。

第三个坑是多工具并行调用的顺序。模型可能一次返回多个工具调用,如果它们之间有依赖(比如先写文件再读文件),并行执行会出错。我的处理是串行执行,按返回顺序一个个来。牺牲一点速度,换来正确性,值得。

6. 扩展方向与个人实践体会

Agent-Reach 现在的形态是个基础框架,真正有意思的是往上加东西。我最近在试的方向是给它加一个"记忆"工具,把重要的对话结论存到本地文件,下次启动时加载。这样 Agent 就有了跨会话的连续性,不用每次从零开始。

另一个方向是工具的组合。比如把"读网页"和"总结"合成一个高层工具,模型调一次就完成两步。这能减少轮数,但也降低了灵活性。我的判断是:高频且固定的操作序列,值得封装成组合工具;低频或需要灵活调整的,保持原子工具。

还有个实用的扩展是加一个--script模式,把用户指令从命令行参数读入,执行完直接退出,不进入交互循环。这样就能把 Agent-Reach 嵌到 shell 脚本或定时任务里,做自动化。实现很简单,就是判断sys.argv,有参数就单次执行。

我个人在实际操作中的体会是,做 Agent 工具最忌讳一上来就追求"全能"。先把三五个核心工具打磨稳,把安全边界划清楚,把错误处理做扎实,比堆一堆花哨功能有用得多。Agent 的能力上限,往往不取决于工具有多少,而取决于每个工具是否可靠、模型是否清楚什么时候该用它。Agent-Reach 这个名字里的 "Reach",说的就是这个——不是够得多远,而是够得稳、够得准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询