从零设计CLI Agent:架构拆解、工具调用与多Agent协同实践

发布时间:2026/10/9 8:34:00
从零设计CLI Agent:架构拆解、工具调用与多Agent协同实践 最近圈子里聊得最多的一个是 Codex CLI一个是 Claude Code但真正动手自己设计一个 CLI Agent 的人其实没有想象中那么多。我是在给团队做内部运维助手时把从命令行入口到业务 Agent 协同这一整条链路都踩了一遍才对“终端形态的 AI Agent”这件事有了完整的认知。这篇内容不是产品测评也不是论文式的架构解读而是把设计过程完整拆给你看CLI Agent 的整体架构、核心原理、流程实现以及最容易翻车的业务 Agent 交互部分。先一句话讲清楚 CLI Agent 是什么。它就是一个跑在终端里的 AI 助手你给它一句话任务它能自己拆解目标、调用工具、执行命令、读取结果、再调整下一步直到任务完成。它跟聊天机器人最大的区别在于聊天机器人只有嘴CLI Agent 有手——它的手是 shell 命令、文件读写、HTTP 请求、数据库查询它的脑子是 LLM 的推理能力。很多团队在 Web 端做 Agent 做得风生水起一到终端就懵了因为终端的交互模型、信息密度和工具生态跟浏览器完全是两码事这不是套个壳就能解决的问题。这篇东西适合正在做或者准备做 AI Agent 的后端工程师、运维和平台组的同学参考。哪怕你只打算用现成的 Codex CLI理解这套底层设计逻辑也能帮你更好地控场知道它在什么情况下会崩、为什么崩、怎么把它掰回正轨。1. 为什么值得自己设计一个 CLI Agent1.1 现成方案够用为什么还要自己造我经常被问这个问题Codex CLI、Claude Code、opencode 这些现成工具已经很好用了为什么还要自己写一个说实话纯粹写代码、改 bug、跑测试这类场景现成工具确实已经覆盖得不错。但你一旦往企业内部的场景走就会碰到几堵墙。第一堵墙是内部系统集成。你团队里的 CI/CD 平台、内部运维系统、自建的数据仓库、内部的告警机器人这些东西没有公开 API或者只有一套很难用的内部 HTTP 接口。现成的 CLI Agent 根本不知道这些系统的存在它连“怎么调用内部发布平台”这种最基本的事情都搞不定。第二堵墙是安全策略。企业内部往往要求命令执行必须留审计日志、敏感信息不能出内网、某些目录不能碰、某些命令不能跑。这需要你在 Agent 的执行层做严格的管控而现成工具很少给你这种程度的定制空间。第三堵墙就是标题里说的业务 Agent 交互。当你已经有了几个业务域的智能体——比如订单分析 Agent、告警处理 Agent、数据报表 Agent——你希望命令行 Agent 作为统一的入口把这些业务 Agent 像乐高积木一样拼起来。这是现成工具目前最难做的事情因为它们的设计目标是通用的编码助手不是企业内部的多智能体编排中心。还有一层原因是我个人的观点自己设计一遍你才会真正理解 Agent 的架构。用别人的工具你只看到了表面自己写一遍你才会对上下文管理、工具调用、停止条件这些核心概念有肌肉记忆。这个价值有时候比工具本身还大。1.2 CLI Agent 和 Web Agent 的本质差异我在设计过程中反复被一个问题折磨为什么不能直接把 Web 端的 Agent 架构搬到 CLI 里答案是两者的交互模型差异太大了。Web 端 Agent 有丰富的视觉反馈。用户能看到按钮、表单、加载动画Agent 可以操纵页面元素点击、输入、等待渲染。CLI 端没有这些它的输入输出都是纯文本流所有信息都靠文字密度来承载。这意味着 CLI Agent 必须更依赖文本结构的组织能力——结果要紧凑、层次要分明、关键信息要在前几行出现否则用户扫一眼终端什么都抓不住。另一个差异是工具执行的距离。Web Agent 通常通过浏览器自动化或调用后端 API 来操作它和真实系统之间隔着一层。CLI Agent 是直接坐在系统里面的它执行的就是系统本地的命令操作的是真实的文件、进程和网络。这个距离近到了危险的程度比如一条rm -rf在 Web Agent 的场景里几乎不会出现但在 CLI 场景里只差一个参数的距离。我用一个表格把差异列一下方便对照记忆维度CLI AgentWeb Agent交互载体纯文本流页面元素与视觉反馈信息密度极高一屏可容纳大量结构化信息相对低依赖布局和图形执行距离直接执行本地命令距离系统最近通常隔一层浏览器或后端 API工具生态shell、文件、进程、管道天然工具化点击、表单、DOM 操作工具化成本高安全风险高一条命令可以改变系统状态相对可控操作范围受制于页面用户期望快速、直接、可脚本化容错性强、可视化、可交互这个差异决定了CLI Agent 的设计核心是“放权和控制”的平衡。你既要让它有足够的工具调用自由度去完成任务又要在执行链路上设置足够多的护栏。这正是后文要展开的架构设计的原点。1.3 典型场景一句话交代终端动手为了后面讨论不那么抽象我先给一个贯穿全文的具体场景。假设你是团队里的运维工程师你在终端里输入“查一下 staging 环境这个小时 5xx 的请求按接口维度聚合给 Top5顺便看下对比上个小时涨了多少。”在没有 Agent 的情况下你需要手动 SSH 到机器、找到日志文件、用 grep 过滤状态码、用 awk 做聚合、再用 sort 排序、最后还得导出来对比上个小时的数据。整个过程如果你是老手可能也要十分钟还要忍受一堆管道命令的转义地狱。有了 CLI Agent这个任务变成了Agent 先自己决定要执行一个日志查询工具然后根据查询结果决定是否需要再执行一个聚合统计工具中间可能需要翻几页日志文件确认格式最后它把结果整理成一份摘要直接回复给你。整个过程你只需要坐在那里看它一步步操作偶尔在它要执行危险命令时点一下确认。这就是 CLI Agent 真正的价值它把“懂业务、懂系统、懂工具”这三件事从人身上转移到了 Agent 身上。而你要设计的就是让这个转移过程可靠、可控、可维护。2. 整体架构一条主线、两层交互、三条管道2.1 核心循环感知、思考、行动、验证CLI Agent 的架构不管怎么包装核心都逃不开一个循环。我给它起了个名叫“感知-思考-行动-验证”四个阶段一轮轮完接着下一轮直到任务完成。感知阶段Agent 把当前能用的所有信息组装起来系统提示词告诉它它是谁、有什么能力、什么不能做对话历史记录之前聊过什么工具描述列表告诉它有哪些工具可以用、每个工具接受什么参数最近一次工具执行结果告诉它刚才那步操作发生了什么。这些信息拼在一起就是 LLM 下一轮决策的全部依据。思考阶段LLM 拿到这些输入后会决定接下来做什么。这里有两种可能一种是它认为任务已经完成直接给出最终答案另一种是它认为还需要执行某个工具于是输出一个结构化的工具调用请求。行动阶段你的代码接管。LLM 是不会自己执行任何操作的系统它只会“提议”。真正去执行 shell 命令或者发 HTTP 请求的是你写在 Agent 框架里的调度代码。这一步要做的包括参数校验、权限检查、命令超时控制、输出捕获。验证阶段工具执行完的结果被捕获后格式化成文本追加到对话上下文里。LLM 在下一轮感知中就能“看到”这次操作的结果然后决定是继续调用工具还是收尾。这个循环是整个 Agent 架构的主线。每循环一次你可以把它理解为 Agent 动了一次手。动手次数越多任务被拆解得越细但风险也越大——每一轮都是一次新的 LLM 调用都会消耗 token 和延迟。2.2 两层交互人机交互与工具交互完整的设计里Agent 并不是自己闷头循环的它要和人打交道也要和系统打交道。这两层交互接口如果不设计好整个 Agent 就像一只没有手脚的章鱼转得再快也干不了活。人机交互这一层是用户能直接感受到的部分。命令行入口要支持两类使用方式一类是单次提问比如agent 帮我查一下当前目录下的 TODO 标记另一类是交互式会话进入一个 REPL 循环像跟人聊天一样连续提问。我强烈建议两个都做因为实际使用中两种场景各占一半。交互式会话里还要考虑流式输出让用户看到文字一行行打出来而不是盯着空白终端等十秒危险操作前需要弹出确认提示用户按 CtrlC 要能优雅中断而不是留下一堆半截操作。工具交互这一层是 Agent 的“手”。工具注册表负责登记所有可用的工具每个工具要有一个名字、一段描述、一个参数结构。调度器负责把 LLM 提出的工具调用请求转成实际的函数执行。执行器则处理具体的操作细节比如 subprocess 的超时、HTTP 请求的重试。这三者的关系是注册表是目录调度器是接线员执行器是干活的人。这两层交互有一个共同的设计原则每一层都要有兜底。人机交互的兜底是中断机制和确认机制防止 Agent 失控工具交互的兜底是超时、错误捕获和输出截断防止单个工具卡死或撑爆上下文。2.3 三条数据管道上下文注入、工具调用、结果回传如果把循环比作心跳那数据管道就是血管。CLI Agent 里有三条管道必须设计清楚缺一条都会出大问题。第一条是上下文注入管道。它负责把系统提示词、对话历史、工具描述、当前任务背景组装成 LLM 的输入。这条管道的核心约束是 token 预算。模型上下文窗口是有限的你要决定哪些信息放进“保险箱”——系统提示词里的安全规则必须永远在工具描述可以按需裁剪历史对话要做滑动窗口工具执行结果要按照重要性决定保留全文还是压缩摘要。第二条是工具调用管道。它负责把 LLM 输出的结构化指令变成真实的函数调用。LLM 输出的是一个 JSON里面包含工具名和参数你的管道要解析这个 JSON、对照注册表找到对应的函数、做参数类型校验、然后执行。这条管道的核心问题是异常处理LLM 可能编造一个不存在的工具名可能传错参数类型可能一次性请求调用五个工具。你的代码必须假设 LLM 会犯错并且优雅地处理这些错误。第三条是结果回传管道。工具执行完结果要以什么形式回到上下文里直接塞原始输出不行原始输出可能长达几万字符。通常的做法是截断到几千字符保留头和尾中间丢了也不影响判断有时候还要做摘要压缩。这条管道的核心是“信息的可消化性”要确保回传的结果是 LLM 能在一次阅读中快速吸收的。三条管道的设计要整体考虑因为它们共享同一个 token 预算池。在实际调试中我见过太多案例是上下文管道做得挺好结果回传管道不设截断几千行日志直接塞进上下文模型立刻“失智”接下来就是无休止的循环调用和幻觉输出。3. 核心原理拆解CLI Agent 为什么能跑起来3.1 工具调用LLM 如何“伸手”干活要理解 CLI Agent绕不开一个核心技术概念工具调用也叫 Function Calling 或 Tool Use。这里有一个常见的误解——很多人以为是 LLM 去执行了某个工具实际上不是。LLM 只是“提议”执行工具真正动手的是你的代码。举一个最简化的例子。你定义了一个工具叫run_shell描述是“执行一条 shell 命令并返回标准输出”。现在用户说“帮我看看当前目录有什么文件”。LLM 不会自己去执行ls它会输出一个结构化的消息大致的 JSON 长这样{ name: run_shell, arguments: { command: ls -la } }你的框架代码拿到这个 JSON 后先解析出工具名run_shell和参数command: ls -la然后在注册表里找到对应的函数把参数传进去执行得到输出结果{ stdout: total 36\ndrwxr-xr-x..., stderr: , code: 0 }这个结果再被格式化成文本追加到 LLM 的对话上下文中作为一条新的“函数返回值”消息。LLM 看到这条结果后就能判断哦我已经知道目录里有什么了现在可以回答用户了。我这么说你就理解了所谓工具调用本质上是 LLM 和你写的代码之间的一种“结构化协议”。LLM 负责决定要做什么你的代码负责真正去做然后做完了告诉 LLM 结果。这个分工非常清晰也非常重要——它意味着即使 LLM 提出了一个危险的操作执行权仍然在你手里你可以在执行之前拦截、确认、或者拒绝。在实现层面现在的各大模型 API 都支持原生的工具调用能力。以 OpenAI 兼容接口为例你在 API 请求里传一个tools参数是一个工具定义的数组每个工具包含名称、描述、JSON Schema 格式的参数结构。模型在推理时会自动判断是不是需要调用工具如果是就在返回的message.tool_calls里给出调用详情。整个过程不需要你用提示词强行引导模型自身就在函数调用和普通回复之间做切换。还有一种更古老的实现方式是 ReAct 模式就是不依赖原生 API 的工具调用能力而是把“思考过程”和“行动指令”都写在文本里比如让模型输出“Action: run_shell\nAction Input: ls -la”然后你的代码解析这段文本。这种方式在前两年很流行但现在已经不推荐了。原生的工具调用在结构上更稳定不会因为模型输出格式漂移而解析失败而且支持一次请求里返回多个工具调用效率高得多。3.2 上下文组装token 预算才是核心工程很多 Agent 项目跑着跑着就废了最常见的死因不是模型不够聪明而是上下文管理失控。我在项目里把上下文组装当成一个独立的工程模块来做而不是顺手写几行代码凑合。Token 预算的分配我有一套自己反复调整后的经验值。假设模型上下文窗口是 128K tokens我一般这样分配系统提示词控制在 1000 到 1500 tokens工具定义控制在 2000 到 4000 tokens对话历史保留最近几轮大约 8000 到 10000 tokens最新的工具执行结果保留 2000 到 4000 tokens剩下的空间全部留给模型“思考”和最终输出。实际比例可以根据你的场景调整但原则是一致的模型的输出空间不能少于总窗口的十分之一否则它还没来得及说完话就被截断了。工具描述的长度是最容易被忽视的隐性消耗。每个工具的描述如果写得太啰嗦几十个工具加起来就是一笔不小的 token 开销。但反过来说描述写得太简略模型就会错误调用工具甚至编造不存在的工具。我的经验是简介里用一两句话说清楚工具能做什么、什么时候用、什么时候别用参数结构用 JSON Schema 描述字段名起得见名知意description 字段写清楚每个参数的类型和边界不要在描述里放示例值那是纯浪费。系统提示词这一块我会把“安全规则”放在最前面确保它在任何上下文压缩策略下都被保留。比如不要执行rm -rf /之类的危险命令涉及生产环境的写操作必须先跟用户确认不要输出密钥和敏感信息。这些规则优先级最高因为上下文压缩策略再怎么优化也不能把这部分裁掉。上下文压缩的策略我也建议做成分层机制。最近的对话完整保留中等距离的对话做摘要久远的对话只保留结论。工具执行结果则可以设置截断阈值只保留前 2000 字符和后 1000 字符中间内容用一行说明代替。这些策略组合起来能保证 Agent 在长会话中不失控。3.3 停止条件什么才算“任务完成”Agent 循环最怕的就是无限转圈。我在早期版本里遇到过不止一次Agent 执行了一个工具、拿到结果、然后又调用同一个工具参数只是微调了一点点如此反复直到把 token 烧完。所以停止条件必须写死。我一般设置四个停止条件任何一个满足就立即终止循环第一个是 LLM 不再请求工具调用直接给出了最终答案。这是最理想的自然收敛说明 Agent 认为自己已经拿到了足够的信息。第二个是达到最大迭代次数。这个次数我默认设为 15 到 20超过就停止然后强制输出当前已知信息的总结。为什么是这个数因为绝大多数任务 5 到 10 轮工具调用就能完成15 到 20 轮已经算是很复杂的任务了。超过这个数大概率是陷入了某种循环再让它跑下去只会浪费 token。第三个是用户主动中断。终端里的 CtrlC 要能被捕获并转化为 Agent 的优雅退出而不是直接杀掉进程导致中间状态丢失。第四个是安全拦截。当工具调用被安全策略判定为危险操作而拒绝时这个事件本身会作为错误信息回传给 LLM但如果这类事件连续触发三次我建议直接中止循环因为说明模型跟这个环境合不来再试大概率还是撞墙。停止条件的实现很简单就是一个带计数器的 while 循环。但设计上需要注意的是每次停止都要有一套清晰的“善后”逻辑——告诉用户为什么停止、当前做到了哪一步、有哪些结果已经拿到。这比默默的停止体验好得多。4. 流程实现从零到一的可运行代码4.1 技术选型与落地理由这一节直接上代码。我用的技术栈是 Python选它的主要理由是团队后端以 Python 为主而且 CLI 生态成熟typer、rich、click 这些库都很好用。如果你是 TypeScript 技术栈下面的设计思路一样可以平移过去核心的代理循环逻辑并不绑定语言。LLM 接入方面我使用的是 OpenAI 兼容接口的 SDK因为现在大多数模型服务商都提供兼容端点切换模型只需要改 base_url 和 model 名框架代码几乎不用动。模型我建议选择支持原生工具调用的这是硬性要求。在写这篇内容的时候市面上主流的几个大模型都已经支持不需要特别担心兼容性。项目结构我分成五个模块刻意保持简单mycli-agent/ ├── cli.py # 入口与参数解析 ├── agent.py # Agent 循环核心 ├── context.py # 上下文组装与压缩 ├── tools/ │ ├── __init__.py # 工具注册表 │ ├── shell.py # shell 执行类工具 │ └── http.py # HTTP 请求类工具 └── config.py # 模型、tokens、安全策略配置这个结构的核心思想是入口、循环、上下文、工具四层解耦。后面要加新工具只需要在 tools 目录里新增一个文件然后用装饰器注册就行要换模型改 config要调循环策略改 agent.py。不会出现“改一行代码要牵连五个文件”的情况。4.2 核心代码工具注册与代理循环先看工具注册表。我用一个装饰器来统一注册工具每个工具只需要声明名字、描述、参数结构然后写好自己的执行函数就行# tools/__init__.py import json from typing import Callable _TOOL_DEFS [] # 给 LLM 看的工具定义 _TOOL_EXECUTORS {} # 给调度器用的执行函数映射 def tool(name: str, description: str, parameters: dict): def decorator(fn: Callable): _TOOL_DEFS.append({ type: function, function: { name: name, description: description, parameters: parameters, } }) _TOOL_EXECUTORS[name] fn return fn return decorator然后定义一个 shell 工具。这里有几个细节超时是必须的默认 30 秒输出要做截断避免把几万行的日志直接塞进上下文返回码要带上LLM 需要知道命令是不是执行成功了# tools/shell.py import subprocess from . import tool tool( run_shell, 执行一条 shell 命令并返回标准输出。适用于文件操作、进程管理、命令查询等场景。, { type: object, properties: { command: {type: string, description: 要执行的 shell 命令}, timeout: {type: integer, description: 超时秒数默认 30} }, required: [command] } ) def run_shell(command: str, timeout: int 30): try: r subprocess.run( command, shellTrue, capture_outputTrue, textTrue, timeouttimeout ) return { stdout: r.stdout[-4000:], stderr: r.stderr[-2000:], code: r.returncode } except subprocess.TimeoutExpired: return {error: f命令执行超时{timeout}s, code: -1}再看 Agent 循环核心。这个循环是整个项目的心脏逻辑其实不复杂组装消息、调模型、判断是否有工具调用、执行工具、追加结果、继续循环。我把代码简化成最小可运行版本# agent.py import json from openai import OpenAI client OpenAI() def dispatch(name: str, args: dict) - str: fn _TOOL_EXECUTORS.get(name) if fn is None: return json.dumps({error: f未知工具: {name}}, ensure_asciiFalse) try: result fn(**args) return json.dumps(result, ensure_asciiFalse, defaultstr)[:8000] except Exception as e: return json.dumps({error: str(e)}, ensure_asciiFalse) def run_agent(user_message: str, max_steps: int 15): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_message} ] for step in range(max_steps): resp client.chat.completions.create( modelMODEL, messagesmessages, tools_TOOL_DEFS, tool_choiceauto, ) msg resp.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content # 自然收敛返回最终答案 for tc in msg.tool_calls: print(f[工具] {tc.function.name}({tc.function.arguments})) result dispatch( tc.function.name, json.loads(tc.function.arguments or {}) ) messages.append({ role: tool, tool_call_id: tc.id, content: result, }) return 已达到最大迭代次数任务可能未完成。请检查上下文或拆分子任务。这套代码跑起来之后你就拥有了一个最简版的 CLI Agent你说一句话它自己决定是否执行 shell 命令反复循环最后给你一个答案。我建议读者拿到代码的第一时间就做一件测试让它执行ls -la看看当前目录然后问它看到了什么。这一步能让你直观理解整个循环是怎么转起来的。4.3 关键实现细节流式输出、权限确认与优雅退出上面的核心代码能跑但离“可用”还有几个关键细节要补。第一个是流式输出。交互会话里用户确实需要看到输出过程每一轮工具调用前的思考过程、工具执行时的提示、最终答案都可以实时打印出来。我在实际实现里用client.chat.completions.create(..., streamTrue)开启流式输出然后把输出按行打印到终端同时用 rich 库给工具调用和结果加上颜色区分。这样用户能清楚看到 Agent 当前是在思考还是在执行工具而不是对着一个凝固的光标干等。第二个是权限确认。shell 工具的执行权是最大的风险面。我在 shell 工具外层加了一层检查如果命令命中危险模式比如rm -rf、mkfs、dd if、写/etc/等或者命令包含疑似密钥和密码的内容就要求 Agent 必须进入“等待确认”状态只有用户在终端按了 Y 才继续执行。这个机制不是不信任 Agent而是给操作加一道人肉栅栏。有一次我们的 Agent 在自动排查问题时差点执行了一条重启生产数据库的命令就是因为这个确认机制拦住了。第三个是优雅退出。终端场景下用户随时可能按 CtrlC我建议在循环外层捕获KeyboardInterrupt然后做收尾处理把当前已经执行的步骤和已获得的结果整理成摘要打印出来提示用户当前状态而不是直接抛异常退出。这个体验细节在日常使用中非常影响好感度因为 Agent 的中间状态是有价值的你不能像杀进程一样把它直接干掉。第四个是对工具结果的截断时机做统一处理。在上面代码里我给dispatch的输出做了 8000 字符截断但实际项目里不同工具的输出价值密度差异很大。日志类工具的结果往往需要保留前几百行来判断问题模式HTTP 返回则要看状态码和响应体的关键字段。我目前的策略是给每个工具的执行函数里自己控制返回的内容dispatch 层只做兜底截断防止个别工具把整个文件内容塞回来。5. 业务 Agent 交互从单兵到协同5.1 为什么要引入业务 Agent而不是把所有工具塞进一个 Agent如果你只是给自己做一个个人效率工具上面那套架构已经够用了。但一旦要进团队、进企业你会面临一个很现实的问题业务系统太复杂一个 Agent 装不下所有领域的知识。拿我们内部的例子来说。团队里有订单数据平台、告警系统、发布系统、客户工单系统。如果我把所有系统的操作工具都塞进 CLI Agent它会变得极其臃肿工具定义占了大量 token模型在几十个工具里选错概率直线上升而且每个系统的对接逻辑都耦合在同一个代码库里任何一个业务变了都要改 Agent 主程序。这个方向是走不通的。正确的思路是让业务系统各有一个自己的业务 Agent。订单分析 Agent 懂订单表结构、会写 SQL、知道异常判定的业务规则告警 Agent 懂告警渠道、值班组、升级策略发布 Agent 懂流水线、灰度策略、回滚流程。CLI Agent 不需要知道这些细节它只需要知道“有这么几个专业 Agent 可以帮忙”以及“怎么把任务交给它们、怎么拿回结果”。这就像你不需要自己会修水管你只需要知道物业的电话号码然后打电话说清楚问题等物业派人来修。引入业务 Agent 之后CLI Agent 的定位就清晰了它是用户和多个专业 Agent 之间的调度中枢负责理解用户意图、拆分任务、分派给合适的专业 Agent、汇总结果、给用户一个完整答复。这个模式在业界有个名字叫编排模式核心价值是让每个 Agent 保持小而专而不是追求一个全能 Agent。5.2 交互协议任务信封与结果信封多个 Agent 之间要协作最重要的不是各自内部怎么写而是它们之间“说什么话、用什么格式说”。如果每个业务 Agent 的接口都长得不一样CLI Agent 就要写一堆适配逻辑接第二个业务系统时又要重写一遍。所以第一步是定义一套标准协议。我采用的是“任务信封 结果信封”这两层结构。任务信封是 CLI Agent 发给业务 Agent 的请求格式{ task_id: task-20250101-001, protocol: agent-task/1.0, agent: order-analysis, intent: query_order_exception, params: { date: 2025-01-01, level: critical, timezone: Asia/Shanghai }, callback: cli://tasks/task-20250101-001/done, timeout_sec: 120 }字段的设计有几个考量。task_id是幂等键业务 Agent 执行重试时不会重复处理同一个任务protocol固定版本号方便以后协议演进时做兼容intent和params分离intent 是业务动作的别名params 是具体参数callback用于异步回调场景但同步模式下可以直接忽略timeout_sec让双方对超时预期达成一致。业务 Agent 处理完之后返回结果信封{ task_id: task-20250101-001, protocol: agent-result/1.0, status: success, data: { exception_count: 12, top_items: [ {interface: /api/order/create, count: 5, pct: 41.7}, {interface: /api/order/query, count: 4, pct: 33.3} ], summary: 订单创建接口异常占比最高疑似库存超卖导致的幂等冲突 }, error: null, cost: {tokens: 3281, latency_ms: 2400} }status只能是success、partial_success、failed三种之一data在成功时是结构化数据在失败时可以为空error失败时给出可读的错误信息cost用于审计和成本追踪。这个信封格式我强烈建议用一个共享的 JSON Schema 库统一管理在 CLI Agent 和业务 Agent 两端同时引用避免两边手写解析逻辑出现偏差。这套协议看起来简单但它解决了一个大问题CLI Agent 与业务 Agent 的耦合关系从“代码级耦合”降到了“协议级耦合”。接一个新的业务 Agent你只需要告诉它这套信封格式然后注册它的能力到服务目录里CLI Agent 的主程序完全不用改。5.3 编排模式串行、并行与递归协议定好了接下来编排逻辑就有得聊了。我在项目里使用了三种编排模式按任务复杂度切换。串行编排是最简单的模式一个任务依赖另一个任务的结果就排队执行。比如用户说“先查一下订单异常的详情然后根据异常类型拟一份告警通知”。CLI Agent 先调用订单分析 Agent 拿异常详情拿到之后把详情的一部分作为输入再调用告警 Agent 生成通知内容。这个流程就是两个业务 Agent 一前一后执行前者的输出是后者的上下文。并行编排用于多个独立任务的同时执行。比如用户问“今天订单异常、支付延迟、退款失败三个系统的状态分别给我汇总一下”。这三个查询互不依赖可以同时发给三个业务 Agent等它们全部返回后CLI Agent 汇总成一份综合报告。并行模式能显著降低整体延迟但实现上要处理部分失败的场景一个 Agent 挂了其他两个依然返回你是等还是不等我的策略是设置一个统一的等待超时超时后已返回的结果照常汇总未返回的标注为“超时未获取”然后连同这个异常状态一起报告给用户不阻塞其他结果。递归编排是最灵活也最危险的一种模式CLI Agent 发现任务太复杂可以自己拆分子任务然后分别派发给业务 Agent再把子结果合起来继续处理。这种模式赋予了 Agent 很大的自由度但也容易失控——它可能拆出十个小任务每个都超时最终拖垮整个会话。我的建议是默认关闭递归编排只有显式授权时才开启并且对子任务数量做硬限制。无论哪种编排模式我都要记录一张任务状态表每发起一个业务 Agent 请求就登记一行任务 ID、目标 Agent、发起时间、状态、返回时间。这张表既是排查问题的基础也是审计留痕的依据。终端里可以用一个实时刷新的状态面板展示这张表用户能直观看到当前有几个任务在跑、哪个卡住了、哪个已经返回。6. 常见问题与排查技巧实录6.1 翻车现场工具调用循环出不来这个坑几乎每个 Agent 项目都会踩。表现是Agent 不停地调用工具每次都是雷同的操作参数只有细微差别就是不收敛。原因通常有三个。第一是工具结果对模型判断没有帮助。比如某个工具返回的是一大段无结构文本模型读了半天不知道下一步该干嘛就只好再试一次同样的操作。解决办法是工具返回结果必须结构化提取出模型真正关心的关键字段。我见过一个日志工具原本返回的是原始日志全文模型反复读也找不到重点后来改成返回“按级别聚合的统计结果”循环问题立刻消失。第二是系统提示词里没写清楚“何时收敛”。我原来的系统提示词只写了“你可以使用工具”模型就倾向于一直用。后来我加了这句话“如果你已经获得了回答用户所需的信息请直接给出最终答案不要再调用额外工具。”就这一句平均对话轮数降了差不多一半。第三是迭代上限设得太高。如果你设了 50 轮模型在 49 轮时还在尝试因为它不知道有这个限制。倒不如设 15 轮每一轮都明确的计数提示模型会更有紧迫感。我在每次工具调用前会打印[Step 3/15]这样的进度标识用户和模型都能感知到剩余次数。6.2 翻车现场上下文爆炸导致模型“失智”上下文爆炸是我排查过最头疼的问题。症状很多样响应速度越来越慢、回答内容开始前后矛盾、甚至出现幻觉编造工具结果。原因就是上下文里塞了太多历史内容模型注意力被稀释了。我的排查第一步永远是看 token 消耗曲线。把每一轮的请求 token 数和回传 token 数记录下来如果曲线在陡峭爬升那基本可以确定是上下文管控失效了。解决方案我按优先级列一下。第一工具执行结果必须截断这条规则在任何情况下都不能破。第二历史对话做滑动窗口只保留最近几轮更早的压缩成一句摘要。第三工具调用细节可以丢弃只保留最终结论。第三点很多人会忽略对话记录里存的是“Agent 调用了某工具返回了某结果”但真正重要的其实是“通过这些调用我们知道了什么”。我会在循环里实时维护一个 facts 列表把关键结论抽出来而不是依赖完整的工具调用历史。6.3 翻车现场业务 Agent 超时与结果丢失业务 Agent 引入之后新的问题来了远程调用不可靠。业务 Agent 可能因为自身负载高、数据库慢、模型超时而迟迟不响应。如果 CLI Agent 同步等待用户会卡在一个无响应的终端里体验极差。我给每个业务 Agent 调用设置了独立的超时时间默认 120 秒超时后立即返回“任务超时”并建议用户稍后重试或查看任务状态接口。同时任务信封里的task_id要用于幂等重试CLI Agent 重发任务时可以带同一个 task_id业务 Agent 根据这个 ID 判断是否已经处理过避免重复执行副作用操作。还有一个容易踩的坑是结果数据大小失控。业务 Agent 可能返回一个非常大的 JSON比如几千条明细数据。如果 CLI Agent 不加限制地把它塞进自己的上下文那上一节说的上下文爆炸又回来了。我的策略是让业务 Agent 在返回时自带摘要字段CLI Agent 只保留摘要和 TopN 明细完整的明细通过“导出到文件”的方式处理需要时再让 Agent 读取文件。6.4 排查工具箱与避坑清单最后分享几个让我省了很多时间的排查工具和方法。第一个是调试开关。我在 CLI 入口加了一个--debug参数开启后会把每一轮的完整消息体包括系统提示词、对话历史、工具定义、模型返回的原始 JSON都输出到一个 JSONL 日志文件里。排查问题时把日志文件喂给大模型让它帮忙分析“模型为什么会这么决策”往往能快速定位到上下文组装或工具描述的问题。第二个是请求重放。上面说的 JSONL 日志里包含了每一次请求的完整输入我写了一个小工具可以直接读取这些请求并重新发送用来验证“同样的输入换了模型版本后输出有什么不同”。这在升级模型时尤其有用能提前发现兼容性问题。第三个是工具调用链的可视化。虽然我不会画复杂的流程图但在终端里用简单的缩进文本就能表示调用链比如[Step 1] run_shell(commandls -la /logs/api-gateway) - stdout: 3 files, 2 dirs [Step 2] query_logs(path/logs/api-gateway, pattern5xx, start14:00) - matched: 128 lines [Step 3] aggregate_by_api(data128 lines) - top5: /api/order/create(52), ...这个缩进结构在排查“Agent 为什么做了多余步骤”的时候很直观。我把这个调用链同时发给用户作为进度显示和存进日志双重利用。避坑清单我再整理几个零散但关键的经验。一个是为每个工具调用添加 trace_id方便把调用链路串起来否则排查时会被并发请求搅得一头雾水。另一个是模型 param 的默认值我建议在 coding 和运维场景下把 temperature 设为 0 或 0.2太高的随机性会让 Agent 在同样输入下做出不同决策这在工程环境里是很糟糕的。还有一个是网络超时重试的退避策略LLM API 调用经常不稳定简单指数退避配合最大重试三次会让整体稳定性上一个台阶。我个人在实际操作中的体会是Agent 的可靠性不来自模型有多强而来自你给它设计的边界有多清晰。工具描述写得越明白上下文管得越死迭代次数卡得越紧它就越像一个可靠的工具而不是一个偶尔灵光的玩具。至于业务 Agent 的接入我的建议是先定义协议再写代码哪怕一开始只接一个业务系统也不要跳过协议设计这一步——等你要接第二个、第三个的时候协议才是真正值钱的东西。CLI Agent 这条路我已经走通了希望这篇拆解能让你少踩几个我踩过的坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询