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作为通信层:为什么不用REST
CLI和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。
- 如果创建Pod,agent服务通过Kubernetes API提交资源清单,Pod启动后通过gRPC回调agent服务,报告状态。
- agent服务把执行结果通过gRPC stream推回CLI,CLI实时渲染。
- 任务结束,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-k,state对象序列化后注入。这样既控制了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; map<string, 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 Plugin:agent用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可能重试
Allocate,Plugin要保证同一个Pod的多次调用返回一致的结果。 - 设备清理:Pod销毁后,Plugin要负责清理设备状态。如果用了MIG或者vGPU,这一步尤其重要。
我的建议是,如果不是非用不可,别自己写Device Plugin。NVIDIA、AMD都有官方的,直接用。自己写只在一种情况下有必要:你有自研的加速硬件,或者需要对设备做特殊的隔离和配额。
4. 实操过程:从零搭一个最小可用的ax原型
4.1 环境准备与依赖安装
先列一下我用的技术栈和版本,避免版本兼容问题:
- Go 1.21+(agent服务和CLI都用Go,跨平台编译方便)
- protoc 3.21+ 和 protoc-gen-go-grpc
- Kubernetes 1.28+(本地用kind或minikube)
- Docker 24+
- 一个LLM的API key(OpenAI兼容接口即可)
安装protoc工具链:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest生成代码:
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服务打包成镜像,部署到Kubernetes:
apiVersion: 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是合理的。
CPU:agent服务本身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_LEVEL=99和GRPC_GO_LOG_SEVERITY_LEVEL=info,能看到底层的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才能稳。