ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CLI Agent实战:从零搭建命令行智能体与多Agent协作

CLI Agent实战:从零搭建命令行智能体与多Agent协作 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.04Python 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: fBearer {API_KEY}}, json{model: MODEL, messages: messages}, timeout60 ) return resp.json()[choices][0][message][content] def execute_tool(name, params): if name run_shell: result subprocess.run( params[command], shellTrue, capture_outputTrue, textTrue, timeout30 ) 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_steps15): 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.jsonAgent 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这东西别指望它一次就完美。它更像是一个需要调教的助手你得花时间跟它磨合。今天调好了一个场景明天可能又冒出个新问题。但每解决一个问题它就变得更可靠一点。这个过程本身也是学习你会更清楚模型的边界在哪里什么样的任务适合交给它什么样的任务还是自己动手更靠谱。
返回列表