资讯详情

资讯详情

从CLI到Kubernetes:AI Agent执行层架构设计与工程实践

1. 从ax这个标题说起一个被低估的CLI工具设计范式第一次看到ax这个标题很多人会以为是某个命令行工具的缩写或者某个内部代号。但把热词铺开来看——Kubernetes、agent、CLI、gRPC、codex cli、claude cli、agent框架、agent记忆、agent安全——你会发现ax其实指向的是一个非常具体的场景用命令行作为入口把AI agent能力接入到已有的工程体系里。它不是一个孤立的工具而是一类agent执行层的设计思路。我自己是从做Kubernetes周边工具开始接触这类东西的。早期我们给集群做运维写一堆kubectl插件后来发现很多重复性的诊断、巡检、变更操作其实可以交给一个agent去跑。但问题来了agent怎么调用怎么保证它不乱来怎么把它的执行过程记录下来这时候ax这种以CLI为壳、以gRPC为里、以Kubernetes为运行底座的架构就浮出水面了。这篇文章我想聊的不是某个具体产品的使用手册而是把ax背后这套CLI agent gRPC Kubernetes的组合拳拆开讲清楚它为什么这么设计、每一步怎么落地、踩过哪些坑。适合正在做agent开发、想给自己的工具加AI能力、或者单纯想搞明白agent到底怎么跑起来的工程师。不管你是刚接触agent的新手还是已经在写agent框架的老手应该都能从里面找到能直接抄的作业。2. 整体架构设计为什么是CLI、gRPC和Kubernetes这三件套2.1 CLI作为入口的合理性低门槛与高可组合性先说CLI。为什么agent的入口要选命令行而不是Web UI或者SDK我自己的体会是三点。第一CLI天然适合自动化和脚本化。你写一个ax run --task diagnose --namespace prod它可以被塞进CI流水线、被cron调度、被其他脚本调用。Web UI做不到这一点SDK虽然能但每次都要写胶水代码。CLI是人机两用的中间态人敲得动机器也调得动。第二CLI的输入输出是文本流天然适配agent的推理链路。agent的核心是读上下文、做决策、执行动作、观察结果这个循环。文本流让每一步都可以被记录、被回放、被diff。你想想codex cli、claude cli这些工具为什么都是命令行形态因为它们的输出要能被管道接走能被重定向到文件能被另一个程序解析。第三CLI的权限模型清晰。一个CLI进程跑在什么用户下、能访问哪些资源、能读哪些文件都是操作系统层面管着的。agent最怕的就是权限失控CLI把这个问题收敛到了进程级别比一个常驻的Web服务好管得多。但CLI也有它的代价。最大的问题是状态管理。命令行进程是无状态的跑完就退出可agent需要记忆——它得记住上一轮做了什么、当前任务进行到哪、有哪些中间结果。这就引出了agent记忆的设计问题后面会专门讲。2.2 gRPC作为通信层为什么不用RESTCLI和agent核心之间怎么通信热词里出现了gRPC而且还有golang grpc helloworld、grpc协议 spring boot、hyperf grpc这些说明跨语言是刚需。选gRPC而不是REST我的理由很实在流式传输。agent执行任务时输出是持续产生的——它可能先思考、再调用工具、再观察、再思考。这种token级别的流用REST的chunked encoding也能做但gRPC的stream是原生支持双向流更是不在话下。CLI可以实时把agent的思考过程打出来用户体验完全不一样。强类型契约。proto文件就是接口文档改字段要重新生成代码编译期就能发现不兼容。REST靠约定容易在联调时扯皮。性能。protobuf的序列化比JSON紧凑对于高频的agent-tool调用省下来的带宽和CPU是可观的。尤其是agent要频繁调用Kubernetes API做查询时这个差距会放大。不过gRPC在Windows下用Visual Studio编译确实是个坑热词里专门有一条grpc在windows下visual studio编译说明踩的人不少。我的经验是优先用vcpkg或者直接拉预编译的二进制别自己从源码编protobuf和abseil的依赖链能把人逼疯。如果非要在VS里编记得把GRPC_BUILD_TESTS关掉不然编译时间翻三倍。2.3 Kubernetes作为运行底座agent的操作系统为什么agent要跑在Kubernetes上因为agent本质是一个需要弹性、需要隔离、需要可观测的长期运行进程。它可能同时处理几十个任务每个任务需要独立的文件系统、独立的网络策略、独立的资源配额。这不就是Kubernetes擅长的事吗具体来说Kubernetes给agent提供了几样关键能力Pod作为执行单元。每个agent任务跑在一个Pod里任务结束Pod销毁环境干净。这比在一个长驻进程里跑多个任务要安全得多一个任务的内存泄漏不会拖垮其他任务。Device Plugin机制。热词里有kubernetes device plugin这个很关键。如果agent需要GPU做推理或者需要特殊的硬件加速卡Device Plugin就是标准接入方式。你写一个Device Plugin把硬件资源暴露给调度器agent的Pod就能声明nvidia.com/gpu: 1这样的资源请求。RBAC和NetworkPolicy。agent能访问哪些API、能连哪些服务全部用声明式配置管起来。这比在代码里写if-else判断权限要可靠得多。但Kubernetes也带来了复杂度。热词里kubernetes 未授权访问漏洞和kubernetes入门指南同时出现说明这个领域的门槛确实存在。我的建议是如果你的agent只是单机跑别上Kubernetes。杀鸡用牛刀运维成本远大于收益。只有当你有多个agent实例需要编排、需要隔离、需要弹性伸缩时Kubernetes才划算。2.4 三者的协作关系一张图讲清楚把这三层串起来数据流是这样的用户在终端敲ax命令CLI解析参数建立到agent服务的gRPC连接。agent服务收到请求根据任务类型决定是在本地执行还是创建一个Kubernetes Job/Pod。如果创建Podagent服务通过Kubernetes API提交资源清单Pod启动后通过gRPC回调agent服务报告状态。agent服务把执行结果通过gRPC stream推回CLICLI实时渲染。任务结束Pod清理结果落盘或入库。这个架构的核心思想是控制面与数据面分离。CLI和agent服务是控制面负责决策和协调Kubernetes里的Pod是数据面负责实际执行。分离的好处是控制面可以很轻数据面可以很重各自独立伸缩。3. 核心细节解析agent执行链路里的关键环节3.1 agent的思考-行动-观察循环怎么落地agent的本质是一个循环。用伪代码表示while not task_done: thought llm.reason(context) action llm.decide_action(thought) result execute(action) context.append(result)听起来简单但每一行都有坑。第一行llm.reason上下文怎么组织把所有历史都塞进去token会爆只塞最近几条agent会失忆。我的做法是分层记忆短期记忆放最近N轮对话长期记忆放向量数据库任务相关的关键信息放一个结构化的state对象。每次推理时短期记忆全量注入长期记忆按相似度检索top-kstate对象序列化后注入。这样既控制了token又保住了关键信息。第二行llm.decide_action怎么让LLM输出结构化的动作最可靠的方式是function calling或者JSON mode。别指望LLM自由发挥输出能被解析的文本一定要用schema约束。我试过让LLM输出请执行kubectl get pods然后正则提取结果它有时候输出我建议执行kubectl get pods有时候输出执行命令kubectl get pods解析逻辑写了一堆还是漏。后来改成function calling定义execute_command(command: str)这样的工具LLM直接返回结构化参数稳得多。第三行execute执行动作时超时和资源限制是必须的。agent可能生成一个死循环的命令或者一个吃满内存的操作。每个动作都要有timeout都要有cgroup限制。在Kubernetes里这就是Pod的activeDeadlineSeconds和resources.limits。第四行context.append观察结果怎么截断命令输出可能几万行全塞回上下文不现实。我的做法是摘要关键行提取。用一个小的LLM或者规则引擎把输出压缩成成功/失败 关键信息 异常行。比如kubectl get pods的输出只保留非Running的Pod和事件信息。3.2 agent记忆的设计短期、长期与工作记忆热词里agent记忆和a-memguard: a proactive defense framework for llm-based agent memory同时出现说明记忆既是核心能力也是安全风险点。我把agent记忆分成三类记忆类型存储位置生命周期用途工作记忆进程内存/Redis单次任务当前任务的中间状态、已执行动作、待办事项短期记忆对话历史单次会话最近N轮交互维持对话连贯性长期记忆向量数据库永久跨会话的知识、经验、用户偏好工作记忆最关键也最容易被忽视。它应该是一个显式的状态机而不是隐式的对话历史。我见过太多agent项目把工作记忆等同于把之前的消息都塞进context结果任务一复杂就乱套。正确的做法是定义一个TaskState对象包含goal、steps_done、current_step、artifacts、errors这些字段每轮循环更新它而不是靠LLM从对话历史里自己悟。长期记忆的安全问题值得单独说。a-memguard那篇论文讲的是记忆投毒攻击者往agent的长期记忆里注入恶意内容agent后续检索到这些内容就会被带偏。防御思路包括写入时做来源验证和内容过滤检索时做相关性阈值过滤使用时做交叉验证。我在实际项目里的做法是长期记忆只存事实性内容不存指令性内容。比如存prod集群的API server地址是xxx不存遇到xxx情况就执行yyy。指令性内容每次从代码或配置里读不经过记忆。3.3 gRPC接口设计proto文件怎么写才不后悔proto文件是agent服务的契约设计不好后期改起来很痛苦。我的几条经验第一请求和响应都要有独立的message不要复用。我见过有人用同一个TaskRequest既做创建又做查询靠字段是否为空来区分后期加字段时兼容性一塌糊涂。第二流式接口要设计好结束信号。gRPC的server streaming客户端怎么知道流结束了靠onCompleted回调。但业务上你可能需要区分正常结束和出错结束所以流里的每条消息应该有一个type字段最后一条是DONE或ERROR。第三错误码要自定义。gRPC内置的status code太粗UNKNOWN涵盖了一切。我通常会在proto里定义一个ErrorCode枚举放在响应的metadata或者消息体里客户端根据它做精细化处理。一个典型的proto长这样service AgentService { rpc ExecuteTask(TaskRequest) returns (stream TaskEvent); rpc GetTaskStatus(TaskStatusRequest) returns (TaskStatusResponse); } message TaskRequest { string task_id 1; string goal 2; mapstring, string params 3; int32 timeout_seconds 4; } message TaskEvent { string task_id 1; EventType type 2; string content 3; int64 timestamp 4; } enum EventType { THOUGHT 0; ACTION 1; OBSERVATION 2; DONE 3; ERROR 4; }这个设计的好处是CLI端可以按type分别渲染——思考用灰色、动作用蓝色、观察用绿色、错误用红色用户体验很直观。3.4 Kubernetes Device Pluginagent用GPU的正确姿势如果agent需要GPU做本地推理Device Plugin是绕不开的。它的工作原理是kubelet通过gRPC调用Device Plugin的ListAndWatch方法Plugin返回可用的设备列表Pod调度时声明资源请求kubelet调用Allocate方法Plugin返回设备路径和环境变量。写一个Device Plugin的核心是实现三个gRPC方法GetDevicePluginOptions、ListAndWatch、Allocate。听起来简单但有几个坑设备健康检查ListAndWatch是一个长连接设备状态变化时要主动推送。如果GPU掉了Plugin要能感知并更新列表否则调度器还会往坏卡上调度。Allocate的幂等性kubelet可能重试AllocatePlugin要保证同一个Pod的多次调用返回一致的结果。设备清理Pod销毁后Plugin要负责清理设备状态。如果用了MIG或者vGPU这一步尤其重要。我的建议是如果不是非用不可别自己写Device Plugin。NVIDIA、AMD都有官方的直接用。自己写只在一种情况下有必要你有自研的加速硬件或者需要对设备做特殊的隔离和配额。4. 实操过程从零搭一个最小可用的ax原型4.1 环境准备与依赖安装先列一下我用的技术栈和版本避免版本兼容问题Go 1.21agent服务和CLI都用Go跨平台编译方便protoc 3.21 和 protoc-gen-go-grpcKubernetes 1.28本地用kind或minikubeDocker 24一个LLM的API keyOpenAI兼容接口即可安装protoc工具链go install google.golang.org/protobuf/cmd/protoc-gen-golatest go install google.golang.org/grpc/cmd/protoc-gen-go-grpclatest生成代码protoc --go_out. --go-grpc_out. proto/agent.proto这里有个坑protoc-gen-go和protoc-gen-go-grpc的版本要匹配不然生成的代码会编译不过。我一般会在go.mod里锁定版本用tools.go的方式管理。4.2 agent服务的核心实现agent服务的主循环我简化成一个可运行的版本func (s *AgentServer) ExecuteTask(req *pb.TaskRequest, stream pb.AgentService_ExecuteTaskServer) error { ctx, cancel : context.WithTimeout(stream.Context(), time.Duration(req.TimeoutSeconds)*time.Second) defer cancel() state : NewTaskState(req.Goal) for !state.Done { select { case -ctx.Done(): stream.Send(pb.TaskEvent{Type: pb.EventType_ERROR, Content: timeout}) return nil default: } thought, err : s.llm.Reason(ctx, state) if err ! nil { stream.Send(pb.TaskEvent{Type: pb.EventType_ERROR, Content: err.Error()}) return nil } stream.Send(pb.TaskEvent{Type: pb.EventType_THOUGHT, Content: thought}) action, err : s.llm.DecideAction(ctx, thought, state) if err ! nil { stream.Send(pb.TaskEvent{Type: pb.EventType_ERROR, Content: err.Error()}) return nil } stream.Send(pb.TaskEvent{Type: pb.EventType_ACTION, Content: action.String()}) result, err : s.executor.Execute(ctx, action) if err ! nil { state.RecordError(err) } stream.Send(pb.TaskEvent{Type: pb.EventType_OBSERVATION, Content: result.Summary}) state.Update(thought, action, result) } stream.Send(pb.TaskEvent{Type: pb.EventType_DONE, Content: state.FinalResult()}) return nil }这段代码的关键点context贯穿始终超时能传导到LLM调用和命令执行。每个事件都实时推送CLI端能立刻看到。state显式更新不依赖对话历史。4.3 CLI端的实现要点CLI端用cobra做命令解析用gRPC client接收流func runTask(cmd *cobra.Command, args []string) { conn, _ : grpc.Dial(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) defer conn.Close() client : pb.NewAgentServiceClient(conn) stream, _ : client.ExecuteTask(context.Background(), pb.TaskRequest{ Goal: args[0], TimeoutSeconds: 300, }) for { event, err : stream.Recv() if err io.EOF { break } if err ! nil { fmt.Fprintf(os.Stderr, error: %v\n, err) break } renderEvent(event) } }renderEvent根据事件类型用不同颜色输出。这里有个细节用ANSI转义码做颜色但要在检测到输出不是TTY时自动关闭不然重定向到文件会有一堆乱码。func renderEvent(e *pb.TaskEvent) { if !isatty.IsTerminal(os.Stdout.Fd()) { fmt.Println(e.Content) return } switch e.Type { case pb.EventType_THOUGHT: fmt.Printf(\033[90m%s\033[0m\n, e.Content) case pb.EventType_ACTION: fmt.Printf(\033[34m%s\033[0m\n, e.Content) case pb.EventType_OBSERVATION: fmt.Printf(\033[32m%s\033[0m\n, e.Content) case pb.EventType_ERROR: fmt.Printf(\033[31m%s\033[0m\n, e.Content) } }4.4 在Kubernetes里跑起来把agent服务打包成镜像部署到KubernetesapiVersion: apps/v1 kind: Deployment metadata: name: ax-agent spec: replicas: 2 selector: matchLabels: app: ax-agent template: metadata: labels: app: ax-agent spec: serviceAccountName: ax-agent containers: - name: agent image: ax-agent:latest ports: - containerPort: 50051 resources: requests: memory: 512Mi cpu: 500m limits: memory: 2Gi cpu: 2 env: - name: LLM_API_KEY valueFrom: secretKeyRef: name: llm-secret key: api-keyServiceAccount的RBAC要配好agent需要创建Job的权限apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: ax-agent-role rules: - apiGroups: [batch] resources: [jobs] verbs: [create, get, list, delete] - apiGroups: [] resources: [pods, pods/log] verbs: [get, list, watch]这里有个安全要点别给agent cluster-admin权限。热词里kubernetes 未授权访问漏洞不是开玩笑的agent如果被prompt injection攻击拿到了高权限后果很严重。最小权限原则必须遵守。4.5 参数计算超时和资源怎么定超时和资源限制不能拍脑袋要有依据。我的计算方法超时统计历史任务P95耗时乘以1.5作为默认超时。比如P95是120秒默认超时设180秒。对于已知的长任务单独配置。内存agent服务本身的内存占用主要是LLM的context。假设context最大32K token每个token约4字节加上中间对象开销约256KB。并发100个任务就是25MB。加上Go runtime和gRPC的开销512MB request、2GB limit是合理的。CPUagent服务本身CPU消耗不高主要是等待LLM API。但如果做本地推理CPU需求会飙升。用Device Plugin挂GPU的话CPU request可以设低一些。5. 常见问题与排查技巧实录5.1 agent执行中断的排查思路热词里agent execution terminated due to error是个高频问题。我整理了一个排查表现象可能原因排查方法解决任务突然中断无输出LLM API超时看agent日志的HTTP状态码增加超时加重试中断前有大量token输出context超限统计每轮token数加摘要减历史中断在某个工具调用后工具执行panic看工具执行的堆栈加recover隔离工具中断在Kubernetes操作时RBAC权限不足kubectl auth can-i补权限随机中断OOM看Pod的exit code 137加内存limit我踩过最深的一个坑是LLM返回的JSON解析失败导致整个任务崩溃。后来改成解析失败时把原始输出和错误信息一起塞回给LLM让它重新生成最多重试3次。这个自愈机制救回了很多任务。5.2 gRPC连接问题的排查gRPC的问题往往很隐蔽。几个常见场景connection refused服务没起来或者端口不对。先telnet或nc测端口。context deadline exceeded超时太短或者服务端处理慢。看服务端的metrics。transport is closing连接被中间设备断了。检查是否有负载均衡器的空闲超时设置。流式调用卡住可能是客户端没读服务端阻塞在Send。检查客户端的Recv循环。我的经验是gRPC的问题一定要开verbose日志。设置GRPC_GO_LOG_VERBOSITY_LEVEL99和GRPC_GO_LOG_SEVERITY_LEVELinfo能看到底层的HTTP/2帧很多问题一目了然。5.3 CLI工具的安装与分发坑热词里codex cli安装、claude code cli安装、unable to locate the codex cli binary or required runtime components这些说明CLI的分发是个大问题。我的做法用Go编译成静态二进制不依赖glibc跨Linux发行版通用。提供多平台构建linux/amd64、linux/arm64、darwin/amd64、darwin/arm64、windows/amd64。用checksum校验发布时提供SHA256安装脚本里校验。别依赖运行时Node.js、Python这些运行时版本问题太多能静态编译就静态编译。如果非要用Node.js写CLI把Node运行时打包进去别让用户自己装。用户装Node的版本五花八门你的CLI在人家机器上跑不起来体验极差。5.4 agent安全的三条红线agent安全是个大话题但有三条红线必须守住第一条命令执行白名单。agent能执行的命令必须是白名单内的。别让LLM自由生成shell命令太危险。我的做法是定义一组工具每个工具是一个具体的操作LLM只能选择工具和填参数不能直接写命令。第二条网络访问限制。agent能访问的网络端点要限制。用Kubernetes的NetworkPolicy只允许访问必要的服务。防止agent被诱导去访问内网敏感服务。第三条敏感信息隔离。agent的context里不能出现密钥、密码、token。这些信息要么放在agent访问不到的地方要么用占位符替换执行时再注入。我见过一个案例agent的context里带了数据库密码结果LLM在思考过程中把密码输出到了日志里。虽然日志是内部的但这也是严重的信息泄露。5.5 性能优化的几个实用技巧agent的性能瓶颈通常在LLM调用但也有一些工程上的优化空间并发执行独立动作。如果agent要查10个Pod的状态这10个查询可以并发。用errgroup或者sync.WaitGroup。缓存LLM的embedding。长期记忆检索时同一个query的embedding可以缓存省一次API调用。流式输出减少感知延迟。用户看到第一个token的时间比总耗时更重要。gRPC的stream让首token延迟降到最低。预热连接。gRPC连接建立有开销agent服务启动时预热到LLM API的连接池。6. 从ax延伸出去agent开发的几条学习路径6.1 agent框架选型自研还是用现成的热词里agent框架、agent开发学习路线、harness和agent区别这些说明很多人在这块纠结。我的观点是如果是学习从零写一个。不写一遍你永远不知道agent的循环里有多少细节。写完之后你对框架的理解会深刻得多。如果是生产用成熟的框架。LangChain、LlamaIndex这些虽然重但生态全工具多。自研框架的维护成本很高除非你的场景非常特殊。harness和agent的区别harness是执行器负责跑命令、管进程agent是决策器负责想下一步做什么。两者可以分离harness可以复用agent可以替换。这种分离设计在测试时特别有用——你可以用mock agent测harness用mock harness测agent。6.2 agent面试题背后的知识体系热词里agent面试题出现说明这个方向已经开始有岗位了。我面过一些人也被人面过总结下来核心考点agent循环的实现能不能手写一个ReAct循环。工具调用的设计function calling的schema怎么写怎么处理调用失败。记忆管理短期和长期记忆怎么划分怎么检索。安全prompt injection怎么防权限怎么控。可观测性怎么追踪一个任务的完整执行链路。这些问题没有标准答案但能看出候选人有没有真正动手做过。6.3 从CLI到平台agent产品的演进路径ax这种CLI形态通常是agent产品的第一阶段。往后演进第二阶段加Web UI。CLI的门槛还是高非技术用户用不了。加一个Web界面把CLI的能力包装成可视化操作。第三阶段加API。让其他系统能集成agent能力提供REST或gRPC API。第四阶段加编排。多个agent协作一个agent负责规划多个agent负责执行。这时候Kubernetes的价值就体现出来了。但我要提醒一句别过早平台化。我见过太多项目第一阶段还没跑通就开始设计平台架构最后什么都没落地。CLI先跑起来有人用了再考虑下一步。6.4 我个人的几个踩坑体会最后分享几个我在做agent相关项目时的真实体会都是文档里不会写的第一LLM的输出永远比你想的更多样。你以为它会输出{action: get_pods}它可能输出{action: get_pods, reason: ...}也可能输出Here is the action: {action: get_pods}。解析逻辑要足够宽容或者用严格的schema约束。第二agent的调试比传统程序难十倍。传统程序出bug看堆栈就行。agent出bug你得看它的思考过程而思考过程是自然语言没有堆栈。所以日志要记全每一轮的prompt、response、action、observation都要落盘方便回放。第三别指望agent一次做对。agent的价值在于它能重试、能调整。设计时要考虑失败路径让agent能从错误中恢复而不是一错就崩。第四成本控制要提前做。LLM API是按token计费的agent循环几轮下来token消耗很快。加一个token预算超了就停别让一个任务烧掉几百块。第五用户的耐心是有限的。agent思考30秒没输出用户就以为卡死了。流式输出、进度提示、心跳这些体验细节决定了用户会不会继续用。这套东西我陆陆续续做了大半年从最初的单机CLI到后来的Kubernetes集群部署中间踩的坑能写一本书。但回头看最核心的东西没变把复杂的事情拆成简单的循环把不确定的事情用确定性的框架兜住。agent再智能也是跑在工程框架里的。框架稳了agent才能稳。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →