☰
CLI Agent实战:从零搭建命令行智能体与多Agent协作
2026/9/29 23:58:58 网站建设 项目流程

1. 为什么我把所有Agent工具都塞进了命令行

第一次接触Agent这个概念是在去年,当时折腾了半天图形界面,点来点去总觉得哪里不对劲。后来一个做后端的朋友跟我说了句大实话:真正干活的东西,最后都会回到命令行。这句话我越想越对。你去看那些真正在生产环境里跑Agent的团队,几乎没有谁天天开着个花哨的网页点按钮,大家都是SSH上去,敲一行命令,看日志滚动,该干嘛干嘛。

CLI-Anything这个思路,说白了就是把Agent的能力从各种花哨的壳子里剥出来,让它回归到最朴素的交互方式——命令行。你可能会问,命令行有什么好的?我列几个我自己踩出来的理由。第一,可组合。命令行天然支持管道,一个Agent的输出可以直接喂给下一个Agent,或者喂给grep、awk这些老牌工具做二次处理。第二,可脚本化。你写个bash脚本就能把一整套Agent工作流串起来,定时跑、批量跑、条件触发跑,比在界面上点来点去靠谱一万倍。第三,可远程。服务器上跑着Agent,你本地只需要一个终端,不用装任何客户端。第四,可版本控制。你的Agent配置、提示词、工作流定义全是文本文件,直接扔进git里管理,谁改了什么一目了然。

但这里有个前提,你得先理解CLI和Agent各自是什么,以及它们为什么能凑到一起。CLI就是命令行界面,你敲命令,程序执行,返回结果。Agent呢,简单说就是一个能自主决策、调用工具、完成任务的智能体。它跟普通的脚本最大的区别在于,脚本是你写死的流程,Agent是它自己决定下一步干什么。把这两者结合,你得到的是一个可以在终端里对话、可以调用本地工具、可以自主完成多步任务的智能助手。

适合谁来参考这篇内容?如果你是个开发者,天天跟终端打交道,想让Agent帮你处理那些重复性的命令行操作,这篇适合你。如果你是个运维,想用Agent来自动化一些巡检、部署、排查的流程,这篇也适合你。如果你是个刚入门Agent开发的新手,想找个轻量级的切入点,命令行是最容易上手的入口。但如果你指望看完就能做出一个能替代你所有工作的超级Agent,那可能会失望,这东西的上限取决于你怎么用。

我自己的使用场景很具体:每天要处理大量的日志分析、文件整理、代码检索、环境检查。以前这些事要么手动敲命令,要么写一堆零散的脚本。现在我把它们统一到一个CLI Agent的框架下,用自然语言描述任务,Agent自己去调对应的工具完成。效率提升不说,关键是心智负担小了很多,不用记那么多命令和参数了。

2. CLI Agent的核心架构拆解

2.1 一个最小可用的CLI Agent由什么组成

很多人一上来就想搞个大而全的框架,结果光配置就劝退了。我建议从最小可用单元开始理解。一个能跑的CLI Agent,核心就四块:输入解析、决策引擎、工具执行、结果输出。

输入解析负责把你敲的自然语言或者结构化命令翻译成Agent能理解的意图。这部分看起来简单,实际上坑很多。比如你说“帮我看看磁盘还剩多少空间”,Agent得知道要去调df命令,而不是傻乎乎地去搜文件。决策引擎是核心,它根据当前上下文和可用工具列表,决定下一步调哪个工具、传什么参数。工具执行就是实际去跑命令或者调API。结果输出把执行结果整理成你能看懂的形式返回。

我自己的做法是,输入解析尽量轻量,不要搞太复杂的NLU,直接用提示词让大模型来理解意图。决策引擎也是靠提示词驱动,把可用工具的描述和当前任务状态一起喂给模型,让它输出下一步动作。工具执行层用子进程调用系统命令,或者用HTTP请求调外部服务。结果输出做一层格式化,该高亮的高亮,该截断的截断。

这里有个关键设计决策:Agent的决策循环要不要设上限?我的经验是必须设。不设上限的Agent就像脱缰的野马,遇到死循环能跑到你怀疑人生。我一般设10到15步,超过就强制终止并返回当前状态。这个数字怎么来的?实测下来,大部分日常任务5步以内能搞定,复杂一点的10步左右,超过15步的基本是出问题了。

2.2 工具注册机制:让Agent知道它能干什么

Agent再聪明,不知道有哪些工具可用也是白搭。工具注册机制就是告诉Agent“你手里有哪些牌”。我见过两种做法,一种是硬编码在提示词里,一种是动态注册。

硬编码简单粗暴,把工具列表直接写进系统提示词。优点是稳定,缺点是加个工具就得改提示词,而且工具多了提示词会爆炸。动态注册更优雅,每个工具是一个独立的模块,启动时扫描注册,运行时按需加载。我倾向于动态注册,虽然初期搭建麻烦点,但后期扩展舒服。

每个工具需要描述清楚几件事:工具名、功能说明、参数列表、返回值格式。功能说明要写得让模型能看懂,别整那些只有人类才懂的缩写。参数列表要标明类型和是否必填。返回值格式最好结构化,JSON或者YAML都行,方便后续处理。

我踩过的一个坑是工具描述写得太模糊。比如有个工具叫“process_file”,描述是“处理文件”。模型完全不知道这工具是干嘛的,该传什么参数。后来改成“读取指定路径的文本文件,返回文件内容,支持txt、md、json格式”,模型立马就知道怎么用了。所以工具描述这件事,宁可啰嗦,不要含糊。

2.3 上下文管理:Agent的记忆怎么存

Agent执行多步任务时,需要记住之前干了什么、得到了什么结果。这就是上下文管理要解决的问题。最简单的做法是把所有历史消息拼成一个长字符串,每次请求都带上。缺点是token消耗大,而且模型容易被无关信息干扰。

我的做法是分层管理。短期上下文只保留最近几轮的关键信息,比如当前任务目标、上一步的执行结果、当前状态。长期上下文存到外部,比如文件或者数据库,需要的时候再检索。这样既控制了token消耗,又保留了必要的信息。

具体实现上,我用一个滑动窗口加摘要的机制。窗口大小设成5轮对话,超过的旧消息用模型生成摘要,压缩成一句话存起来。这样即使跑几十步的任务,上下文也不会爆。实测下来,这个方案在成本和效果之间平衡得不错。

还有一个细节是上下文里的工具调用结果要不要全量保留。我的经验是,大结果只保留摘要和关键字段,完整结果存到临时文件,需要的时候让Agent自己去读。比如你跑了个命令返回几千行日志,全塞进上下文纯属浪费,不如告诉Agent“结果已存到/tmp/xxx.log,共1234行,前10行是...”,它需要细节的时候自己去读文件。

3. 从零搭建一个CLI Agent的实操过程

3.1 环境准备与依赖安装

先说环境。我用的是一台Linux开发机,Ubuntu 22.04,Python 3.11。为什么用Python?因为生态好,调模型、跑子进程、处理文本都方便。Node.js也行,但Python在这类任务上写起来更顺手。

依赖方面,核心就几个:一个HTTP客户端用来调模型API,一个命令行解析库用来处理参数,一个子进程管理库用来跑系统命令。我用的组合是httpx、argparse、subprocess。都是标准库或者轻量级库,不引入重型框架。

安装过程没什么好说的,pip install httpx就完事了。但有个坑要注意:如果你在macOS上,系统自带的Python版本可能比较老,建议用pyenv或者conda装个新版本。Windows上更麻烦,子进程调用和路径处理跟Linux差异很大,建议用WSL或者直接在Linux服务器上开发。

模型API这块,我用的是兼容OpenAI接口的服务。你需要准备一个API key,配好base_url和model name。这些信息放在环境变量里,别硬编码在代码里。我见过有人把key直接写在脚本里然后传到公开仓库,后果不用我多说。

3.2 核心循环的代码实现

核心循环的逻辑很直白:接收用户输入,拼上下文,调模型,解析模型返回的动作,执行动作,把结果拼回上下文,判断是否结束,没结束就继续循环。

import httpx import subprocess import json import os API_KEY = os.environ.get("AGENT_API_KEY") BASE_URL = os.environ.get("AGENT_BASE_URL", "https://api.example.com/v1") MODEL = os.environ.get("AGENT_MODEL", "gpt-4") TOOLS = { "run_shell": { "description": "执行shell命令并返回输出", "params": {"command": "要执行的命令字符串"} }, "read_file": { "description": "读取指定路径的文本文件", "params": {"path": "文件路径"} }, "write_file": { "description": "将内容写入指定文件", "params": {"path": "文件路径", "content": "要写入的内容"} } } def call_model(messages): resp = httpx.post( f"{BASE_URL}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": MODEL, "messages": messages}, timeout=60 ) return resp.json()["choices"][0]["message"]["content"] def execute_tool(name, params): if name == "run_shell": result = subprocess.run( params["command"], shell=True, capture_output=True, text=True, timeout=30 ) return result.stdout + result.stderr elif name == "read_file": with open(params["path"], "r") as f: return f.read() elif name == "write_file": with open(params["path"], "w") as f: f.write(params["content"]) return "写入成功" return "未知工具" def agent_loop(user_input, max_steps=15): messages = [ {"role": "system", "content": build_system_prompt()}, {"role": "user", "content": user_input} ] for step in range(max_steps): response = call_model(messages) action = parse_action(response) if action["type"] == "final": return action["content"] result = execute_tool(action["name"], action["params"]) messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"执行结果:{result}"}) return "达到最大步数限制,任务终止"

这段代码是简化版,实际用的时候还要加错误处理、日志、超时控制。但核心逻辑就这些。parse_action函数负责从模型返回的文本里提取出结构化的动作,我一般要求模型返回JSON格式,解析起来最稳。

build_system_prompt函数构造系统提示词,把工具列表和输出格式要求写进去。提示词的质量直接决定Agent的表现,这个后面单独说。

3.3 提示词设计的几个关键点

提示词是Agent的灵魂。我调了大概几十版才找到一个比较稳定的写法。核心原则是:明确角色、明确工具、明确输出格式、明确边界。

角色定义要具体。别说“你是一个助手”,要说“你是一个命令行Agent,负责在Linux环境下完成文件操作、命令执行、信息检索等任务”。越具体,模型的行为越可控。

工具描述要完整。每个工具的名字、功能、参数、返回值都写清楚。我还会加一句“只使用上面列出的工具,不要编造不存在的工具”。这句话能减少很多幻觉。

输出格式要强制。我要求模型每次返回一个JSON对象,包含type字段(action或final)、name字段(工具名)、params字段(参数)、content字段(最终答案)。解析的时候如果JSON格式不对,就返回错误让模型重试。

边界要明确。比如“不要执行删除操作除非用户明确要求”、“不要访问网络除非任务需要”、“单次命令执行时间不超过30秒”。这些约束能防止Agent干出一些你不想看到的事。

我踩过的一个坑是提示词太长导致模型注意力分散。后来我把提示词拆成两部分,核心规则放系统提示词,任务相关的上下文放用户消息里。这样模型更容易聚焦。

4. 多Agent协作在CLI下的实现方式

4.1 什么时候需要多个Agent

单个Agent能搞定的事,别搞多个。这是我一贯的原则。多Agent带来的复杂度是指数级上升的,通信、协调、状态同步,每一个都是坑。但有些场景确实需要多个Agent,比如任务本身可以并行、不同子任务需要不同的工具集、或者需要多个视角来交叉验证。

我遇到的一个典型场景是代码审查。一个Agent负责读代码找问题,一个Agent负责跑测试验证,一个Agent负责查文档确认API用法。三个Agent各干各的,最后汇总结果。这种场景下,单Agent串行做也能做,但并行做更快,而且每个Agent的提示词可以更专注。

另一个场景是长流程任务。比如部署一个服务,涉及环境检查、依赖安装、配置生成、服务启动、健康检查。这些步骤可以拆给不同的Agent,每个Agent负责一段,通过共享文件或者消息队列来传递状态。

4.2 用管道和文件做Agent间通信

CLI环境下,Agent间通信最自然的方式就是管道和文件。管道适合流式数据,文件适合结构化数据。

管道的方式很简单,Agent A的输出直接作为Agent B的输入。比如:

agent-a "分析日志文件 /var/log/app.log 找出错误" | agent-b "根据错误信息生成修复建议"

这种方式适合简单的串联。但有个问题,Agent A的输出格式得是Agent B能理解的。所以我在设计的时候会约定一个中间格式,比如JSON Lines,每行一个JSON对象,包含type、content、metadata字段。

文件的方式更灵活。Agent A把结果写到/tmp/agent-a-output.json,Agent B从那里读。好处是解耦,Agent B不关心Agent A是怎么产生的输出,只关心文件格式。坏处是要管理文件的生命周期,别忘了清理。

我一般用文件方式做复杂协作,管道方式做简单串联。文件放在一个共享的工作目录下,每个Agent有自己的命名空间,避免冲突。

4.3 协调者的角色与实现

多Agent系统里,协调者很重要。它负责分配任务、收集结果、处理异常。协调者本身也可以是一个Agent,但它的工具集跟执行Agent不一样,主要是调度类的工具。

我的做法是写一个简单的调度脚本,不一定要用Agent来做协调。脚本读任务列表,按依赖关系排序,依次或并行启动执行Agent,收集结果,判断是否继续。这样比让Agent自己协调更可控。

但如果任务本身需要动态决策,比如根据中间结果决定下一步干什么,那协调者用Agent来做更合适。这时候协调者的提示词要写清楚它的职责:只做调度,不做具体执行,遇到不确定的情况就停下来问人。

我踩过的一个坑是协调者Agent越权去执行具体任务,导致职责混乱。后来我在提示词里明确写了“你只负责分配任务和收集结果,不要自己执行任何具体操作”。这句话加进去之后,行为就正常了。

5. 常见问题与排查技巧实录

5.1 Agent执行终止或报错的排查思路

Agent跑着跑着突然停了,或者报了个莫名其妙的错,这是最常见的问题。我的排查顺序是这样的:先看日志,确认停在哪一步;再看那一步的输入输出,确认是模型的问题还是工具的问题;最后看上下文,确认是不是token超了或者格式乱了。

日志这块,我建议每一步都打详细日志,包括请求的messages、模型返回的原始内容、解析后的动作、工具执行的结果。别嫌日志多,出问题的时候你就知道有用了。

模型的问题通常是这几种:返回格式不对、调用了不存在的工具、参数类型错了、陷入死循环。格式不对就加强提示词里的格式约束,调用不存在的工具就检查工具列表是不是没更新,参数类型错了就在工具描述里写清楚类型,死循环就加步数限制和循环检测。

工具的问题通常是这几种:命令不存在、权限不够、超时、返回结果太大。命令不存在就检查环境变量和PATH,权限不够就检查用户和文件权限,超时就加超时时间或者优化命令,结果太大就做截断或者存文件。

5.2 工具调用失败的典型场景与修复

我整理了一个速查表,覆盖了我遇到的大部分工具调用失败场景。

问题现象可能原因排查方法修复方案
命令找不到PATH不对或未安装which命令名安装依赖或修正PATH
权限拒绝用户权限不足ls -l看文件权限调整权限或换用户
执行超时命令耗时过长手动跑一遍计时加超时或优化命令
输出乱码编码不一致file命令看编码统一用UTF-8
结果截断缓冲区太小看输出长度存文件或分页读取
参数解析错引号或转义问题打印实际命令用列表传参代替字符串

这个表我贴在显示器旁边,出问题的时候对照着看,大部分情况能快速定位。

还有一个隐蔽的坑是环境变量不一致。你在终端里跑命令没问题,Agent跑就报错,很可能是Agent的环境变量跟你的shell不一样。解决办法是在Agent启动时显式设置需要的环境变量,或者用绝对路径调命令。

5.3 性能优化的几个实用技巧

Agent跑得慢,通常是三个原因:模型调用慢、工具执行慢、上下文太大。模型调用慢没办法,换更快的模型或者减少调用次数。工具执行慢就优化命令,比如用更高效的参数、加缓存、并行执行。上下文太大就做摘要和裁剪。

我自己的优化经验是,把不依赖模型结果的工具调用提前并行跑。比如任务需要读三个文件,这三个读操作可以同时进行,不用等模型一个一个决策。实现上就是在执行层加一个并行执行的能力,模型一次返回多个动作,我并行执行完再一起返回结果。

还有一个技巧是缓存模型调用。同样的输入如果之前调过,直接返回缓存结果。这在调试阶段特别有用,能省不少token和时间。但要注意缓存失效策略,任务状态变了缓存就得清。

上下文裁剪我一般用滑动窗口加摘要。窗口大小根据模型的能力定,一般保留最近5到10轮。更早的用模型生成摘要,压缩成几句话。摘要的质量很关键,我一般要求摘要包含:已完成的关键步骤、当前状态、待解决的问题。

6. 我踩过的坑和总结的经验

6.1 安全边界怎么设才靠谱

Agent能执行命令,这本身就是个风险。我给自己定了几个硬规矩。第一,危险命令黑名单,rm -rf、mkfs、dd这些直接拦截,不管模型说什么都不执行。第二,工作目录限制,Agent只能在指定的目录下操作,不能跑到系统目录去。第三,网络访问限制,除非任务明确需要,否则不允许Agent发起网络请求。第四,敏感信息保护,环境变量里的key、密码这些不让Agent读到。

这些限制写在工具执行层,不依赖模型的自觉。模型可以被提示词约束,但提示词是可以被绕过的,硬编码的检查绕不过去。

我见过有人让Agent直接跑在root下,没有任何限制,结果Agent一个误操作把系统搞崩了。这种教训一次就够了,别自己去试。

6.2 调试Agent的实用方法

调试Agent跟调试普通程序不一样,因为它的行为是不确定的。同样的输入,两次运行可能走不同的路径。我的调试方法是:固定随机种子(如果模型支持)、记录完整轨迹、复现问题、逐步缩小范围。

记录完整轨迹很重要。我会把每一步的messages、response、action、result都存下来,出问题的时候回放。回放的时候可以手动修改某一步的输入,看模型会怎么反应,这样能快速定位是哪个环节出了问题。

复现问题有时候很难,因为模型的行为有随机性。我的做法是把出问题的输入和上下文固定下来,反复跑,看问题出现的概率。如果每次都出,那就是确定性问题,好修。如果偶尔出,那就是概率问题,得加强约束。

逐步缩小范围就是二分法。把任务拆成几步,看问题出在哪一步。或者把上下文删减,看删到哪部分问题就消失了。这个方法笨但有效。

6.3 从单Agent到多Agent的演进路径

我的建议是,先把单Agent跑通,再考虑多Agent。单Agent都跑不稳,多Agent只会更乱。单Agent跑通的标志是:常见任务能稳定完成、错误能正确处理、性能可接受。

演进到多Agent的时机是:任务可以明显并行、不同子任务需要不同工具集、单Agent的提示词已经复杂到难以维护。这时候拆成多个Agent,每个Agent的职责更单一,提示词更简单,反而更好维护。

拆的时候注意几点:Agent之间的接口要定义清楚,输入输出格式要固定;协调逻辑要简单,别搞太复杂的调度;错误处理要统一,一个Agent挂了不能影响其他Agent;状态管理要集中,别让每个Agent自己管自己的状态。

我自己的项目从单Agent演进到三Agent用了大概两个月。第一个月把单Agent跑稳,第二个月开始拆。拆的过程中最大的感受是,协调逻辑比执行逻辑难写多了。执行逻辑是线性的,协调逻辑要考虑各种异常和边界情况。

6.4 一些零散但有用的心得

提示词里的示例比描述更有用。给模型看一个完整的输入输出示例,比写一段描述效果好得多。我一般会在提示词里放两到三个示例,覆盖正常情况和边界情况。

工具的数量不要太多。我试过给Agent注册二十多个工具,结果模型经常选错。后来精简到八个核心工具,准确率明显提升。工具多了不仅模型容易懵,维护成本也高。

日志的级别要可调。调试的时候开DEBUG,看所有细节。生产环境开INFO,只看关键步骤。别一直开DEBUG,日志文件会爆炸。

超时时间要分层设置。模型调用超时设长一点,60秒左右。工具执行超时设短一点,30秒左右。整个任务的总超时设更长,比如5分钟。这样既能处理慢操作,又不会无限等待。

版本控制要跟上。Agent的提示词、工具定义、配置文件都要进git。每次改动都提交,出问题的时候能回滚。我吃过亏,改了一版提示词效果变差了,想回滚发现没存旧版本,只能凭记忆重写。

测试用例要积累。每次遇到一个典型问题,就把它做成一个测试用例。跑回归测试的时候一次性验证所有用例,确保改动没有引入新问题。我的测试集现在有三十多个用例,覆盖了大部分常见场景。

最后说一个心态上的体会。Agent这东西,别指望它一次就完美。它更像是一个需要调教的助手,你得花时间跟它磨合。今天调好了一个场景,明天可能又冒出个新问题。但每解决一个问题,它就变得更可靠一点。这个过程本身也是学习,你会更清楚模型的边界在哪里,什么样的任务适合交给它,什么样的任务还是自己动手更靠谱。

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

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

立即咨询