
CrashLoopBackOff 排障助手的 Prompt 能涨到 300K Token这事是在单次诊断超过 15 秒、exitCode:137 被几万行日志淹没时才被正视的。模型通道我改走 TaoTokenKey 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建K8s 助手里大模型客户端的 Base URL 填 https://taotoken.net/apiK8s API 侧则交给 client-go 的 ExtractMinimalContext 做裁剪只留 metadata.name、status.phase、containerStatuses[].lastState、最近一条 Warning Event 和 tail 20 行日志再用 BuildPrompt 拼 Runbook。首版把 CRD 描述、Prometheus 历史指标、Chat 历史全部拿掉只保留这条最小闭环。原文给的判断很直接首版不是做全量知识问答而是把「Pod 现在怎么了」回答清楚。全量字段看起来更聪明实际让模型在噪声里找信号延迟和成本先崩exitCode:137 这种被截断在日志后面的关键行反而因为上下文被挤爆而排不到前面。所以这篇不聊大而全的平台设计只做两件事一用 client-go 的五个取数口把诊断上下文压到 1K Token 附近二把模型调用侧指向 TaoToken 的兼容通道让 Key、Base URL、模型 ID 三件套先跑通。下面按首版保留清单往下走。1. CrashLoopBackOff 首版 Prompt 是怎么堆到 300K Token 的1.1 CRD、Prometheus、Chat 历史、几万行日志一起塞进去第一版写得很「努力」诊断开始前先把 CRD 结构拉全怕模型不认识自定义资源再补一段 Prometheus 过去几小时的历史指标想让模型判断是不是资源抖动Chat 历史整段保留方便连续追问容器日志直接取了上万行理由是「万一出错行在很早以前」。这些数据单看都有道理放在一次 /api/v1/diagnose 里就变成灾难。CRD 描述里大量与本次 Pod 无关的 schemaPrometheus 序列点几千条时间戳和数值占了大头Chat 历史让每次请求都带着上一次甚至上几次的内容日志更夸张CrashLoopBackOff 的容器本来就在反复重启几万行里大部分是重复的重试输出。这些内容拼进 Prompt 后Token 曲线几乎是垂直上升。首版没有做字段白名单也没有做长度截断所有内容都走同一条模板。模型收到的上下文里真正有用的信息可能只有几十行Pod 名字、阶段、上次退出码、最近一条 Warning加上崩溃前的最后几行日志。其余部分不是没用是不该在首版的一次诊断请求里出现。这个判断很关键首版该保留的不是「模型可能想看的所有东西」而是「没有它就没法给出下一步命令的东西」。1.2 15 秒以上和 exitCode:137 被淹没哪个更致命单次请求超过 15 秒是体感问题Token 成本高是账单问题exitCode:137 被淹没才是诊断质量问题。排障助手最需要的动作是给出可验证的只读命令例如 describe 看 Last State、logs --previous 看被 kill 前的输出、get events 看 OOMKilled 或探针失败。如果 Prompt 里塞了太多无关内容模型注意力被稀释输出的建议容易变成泛泛的「检查资源限制」「查看日志」而不会针对 exitCode:137 追问是不是内存超限、是不是探针把容器重启掉。另外15 秒以上的延迟会直接改变使用方式。本地排障时工程师愿意给一次诊断 3 到 5 秒超过这个时间就会切回 kubectl一旦诊断入口被绕过助手就失去了迭代机会。首版的目标不是覆盖所有故障类型而是让 CrashLoopBackOff 这条最高频的路径先达到可用的响应和可读的输出。Token 成本高则会让灰度阶段不敢放开调用量最后变成「有功能但没人用」。所以裁剪不是优化项而是首版能不能上线的前提。2. 首版只留 client-go 裁剪ExtractMinimalContext 的五个取数口2.1 metadata.name、status.phase、containerStatuses[].lastState 怎么取裁剪的第一步是把「Pod 对象里真正与 CrashLoopBackOff 相关的字段」列出来。metadata.name 用来定位对象status.phase 先判断是 Pending、Running 还是 FailedcontainerStatuses[].lastState.terminated 则给出上次终止的原因和退出码restartCount 顺带带上帮助模型判断重启频率。这里不要取整个 Pod YAML也不要取 spec 里所有字段首版只要这四五个字段就够模型建立基本判断。下面这段 ExtractMinimalContext 只做读取不做任何写操作也不替模型执行命令。代码里对多个容器做了简单处理取第一个有上次终止状态的容器如果你的场景需要区分 sidecar可以在这里加容器名白名单但首版不建议把逻辑做复杂。type MinimalContext struct { Name string json:name Phase string json:phase LastReason string json:lastReason ExitCode int32 json:exitCode RestartCount int32 json:restartCount LastWarning string json:lastWarning LogTail []string json:logTail } func ExtractMinimalContext(ctx context.Context, cs kubernetes.Interface, ns, pod string) (*MinimalContext, error) { p, err : cs.CoreV1().Pods(ns).Get(ctx, pod, metav1.GetOptions{}) if err ! nil { return nil, fmt.Errorf(get pod: %w, err) } mc : MinimalContext{Name: p.Name, Phase: string(p.Status.Phase)} for _, cst : range p.Status.ContainerStatuses { last : cst.LastTerminationState.Terminated if last nil { continue } mc.LastReason last.Reason mc.ExitCode last.ExitCode mc.RestartCount cst.RestartCount break } return mc, nil }注意这段代码只负责拿字段不要在里面顺手打印整个 Pod一旦日志里出现全量对象调试时很容易又把大对象带回 Prompt。字段读出来之后下一步才是补事件和日志尾部。2.2 Warning Event 只取最近一条日志只 tail 20 行事件列表很容易失控。K8s 里同一个 Pod 会反复产生事件CrashLoopBackOff 场景下 BackOff、Unhealthy、Killing 会交替出现。如果直接把 events list 全量交给模型Token 立刻上去而且早期事件会盖住最新状态。首版的做法是按 LastTimestamp 倒序只取最新一条 Type 为 Warning 的事件拼成简短字符串。日志同理tail 20 行并加上 Previous: true优先看上一个已终止容器的输出因为当前容器可能刚起来还没有日志。func attachLastWarningAndTail(ctx context.Context, cs kubernetes.Interface, ns, pod string, mc *MinimalContext) error { evs, err : cs.CoreV1().Events(ns).List(ctx, metav1.ListOptions{ FieldSelector: involvedObject.name pod ,involvedObject.namespace ns, }) if err ! nil { return err } sort.Slice(evs.Items, func(i, j int) bool { return evs.Items[i].LastTimestamp.Time.After(evs.Items[j].LastTimestamp.Time) }) for _, e : range evs.Items { if e.Type corev1.EventTypeWarning { mc.LastWarning fmt.Sprintf(%s: %s, e.Reason, strings.TrimSpace(e.Message)) break } } tail : int64(20) raw, err : cs.CoreV1().Pods(ns).GetLogs(pod, corev1.PodLogOptions{ TailLines: tail, Previous: true, }).DoRaw(ctx) if err ! nil { return err } for _, line : range strings.Split(string(raw), \n) { if s : strings.TrimSpace(line); s ! { mc.LogTail append(mc.LogTail, s) } } return nil }这里有个容易忽略的边界如果容器从来没有成功启动过Previous 可能取不到日志函数会返回错误。首版不要让整个诊断失败可以把错误变成一条「previous log unavailable」的提示继续把其他字段交给模型。裁剪的目的是降低噪声不是让取数失败把主流程打断。2.3 BuildPrompt 的 Runbook 模板与输出约束字段裁剪完Prompt 模板也要跟着瘦。首版只保留一个固定 Runbook 模板不要让模型自由生成结构。模板里明确告诉模型你只输出只读检查建议不要输出删除资源、重启 Deployment、改探针这类高风险动作每个原因后面给出对应的 kubectl 命令如果信息不足直接说还缺哪一项而不是编一个原因。const runbookTmpl 你是 Kubernetes 排障助手只输出只读检查建议不执行任何命令。 Pod: {{.Name}} Phase: {{.Phase}} 上次终止: {{.LastReason}} (exitCode{{.ExitCode}}, restarts{{.RestartCount}}) 最近 Warning: {{.LastWarning}} 最近 20 行日志: {{range .LogTail}} {{.}} {{end}} 请按以下格式回答: 1. 最可能的三个原因按概率排序并说明依据。 2. 针对每个原因的只读 kubectl 命令例如 describe、logs --previous、get events。 3. 需要人工确认的配置项不要给出删除或改动生产资源的命令。BuildPrompt 阶段只做模板填充不再拼接 CRD、Prometheus 指标和 Chat 历史。如果后续确实要支持多轮追问也建议把历史压缩成「上一轮给出的命令 用户贴回的结果」而不是整段 Chat 历史原文带下去。首版把这个口子关掉Token 曲线才会稳定。3. 模型通道换到 TaoTokenKey、Base URL 和 Go 客户端初始化3.1 先去官网创建 Key 并选模型K8s 助手本身不关心模型从哪来它只认一个兼容接口。首版把模型调用统一收口到一个环境变量里所以迁移成本很低打开 TaoToken 注册并创建 API Key然后在模型广场挑一个适合排障问答的模型 ID。这里不要凭记忆写模型名也不要在代码里硬编码一个带日期后缀的 ID模型列表会变写死之后某天找不到模型诊断入口会直接 404。创建完成之后你会拿到一把 Key把它放进环境变量或本地的 secret 文件不要提交到 Git。模型 ID 同样走环境变量部署时再决定用哪个。首版建议先固定一个模型把 Prompt 模板和输出格式调顺再考虑按故障类型路由不同模型。3.2 Base URL 填 https://taotoken.net/api末尾不要加 /v1模型客户端的 Base URL 统一写 https://taotoken.net/api末尾不要加 /v1。很多 OpenAI 兼容 SDK 会在 BaseURL 后面自己拼 /v1/chat/completions如果这里再写成 https://taotoken.net/api/v1实际请求就会变成 /api/v1/v1/chat/completions通道侧只能返回错误。这个错误在本地调试时经常被误判成 Key 失效实际检查一下地址末尾就能排掉。另外要区分「给人点的官网」和「给程序填的接口」注册、创建 Key、看模型广场、看用量走 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 程序里的 Base URL 只填 https://taotoken.net/api。两者不要混用更不要把官网链接的查询参数带进 SDK 配置。3.3 环境变量与 openai 兼容客户端示例首版用环境变量把 Key、Base URL、模型 ID 三件套注入诊断服务代码里不出现任何明文密钥。下面这段客户端初始化只创建请求通道不从环境里读生产配置去执行命令。export TAOTOKEN_API_KEYYOUR_API_KEY export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_MODEL_IDYOUR_MODEL_IDpackage llm import ( os openai github.com/sashabaranov/go-openai ) func NewDiagnoseClient() *openai.Client { cfg : openai.DefaultConfig(os.Getenv(TAOTOKEN_API_KEY)) cfg.BaseURL os.Getenv(TAOTOKEN_BASE_URL) return openai.NewClientWithConfig(cfg) }调用时把 BuildPrompt 的结果作为 user 消息system 消息固定成「你是 Kubernetes 排障助手」。模型 ID 填环境变量里的值如果你不确定该写哪一个以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场当时列表为准。YOUR_API_KEY 同样从那个页面创建不要复用其他平台的 Key也不要把它写进镜像。4. 用 time curl /api/v1/diagnose 验收 Prompt 是否回到 1K Token 附近4.1 一次诊断请求要看的三个字段配置改完不要直接接前端先用 curl 打本地诊断接口确认 Prompt 收敛、延迟可接受、返回内容包含 kubectl 建议。命令里的地址是 K8s 助手自己的 /api/v1/diagnose不是模型通道的 Base URL这一点经常有人搞混结果把诊断接口的路径当成模型地址填进 SDK。time curl -sS -X POST http://127.0.0.1:8080/api/v1/diagnose \ -H Content-Type: application/json \ -d {namespace:default,pod:demo-7f9c8b6d5-abcde} \ | jq {prompt_tokens, completion_tokens, latency_ms, prompt_preview: .prompt_preview}看三个数prompt_tokens 是否回到 1K Token 附近latency_ms 是否比首版明显下降prompt_preview 里是否只有 Pod 基础字段、最近 Warning 和 20 行日志。如果 prompt_tokens 还在几万以上先别怀疑模型通道回到 ExtractMinimalContext 检查是不是又拼了全量对象或全量事件。4.2 P95 和 Token 收敛的判定口径单次请求好看不代表整体达标。把同一组 CrashLoopBackOff 样例连续打几十次看诊断 P95 是否达到你们服务自己的 SLO。首版验收口径建议写成两条第一prompt_tokens 稳定在 1K Token 附近不随日志量线性增长第二诊断 P95 相比首版全量上下文明显下降且输出里必须出现针对 exitCode 的追问或对应检查命令。不要只看平均值平均值会被少数快速请求拉低P95 更能反映工程师体感。验证过程中可以打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 看这次模型调用的记录和用量确认请求确实走了你配置的 Key 和模型。如果用量对不上优先检查诊断服务是否还在用旧的环境变量或旧镜像。验证通过后再把诊断入口接到聊天前端不要带着未收敛的 Prompt 直接灰度。5. 裁剪后还容易翻车的口子Event 全量、日志 tail、通道参数5.1 Event List 没有按时间截断Warning 又被冲掉最常见的是事件列表没有按时间倒序就交给模型或者为了「保险」把最近 50 条事件都拼进去。CrashLoopBackOff 会持续产生事件50 条里可能大部分是重复的 BackOff真正有用的 Warning 被挤在中间。ExtractMinimalContext 里只取最新一条 Warning 就够首版用了如果你担心漏掉 OOMKilled 和探针失败同时出现可以按 Reason 去重后取最近三条但不要再扩到全量。另一个细节是日志 tail 参数。TailLines 设成 20 是首版边界不要在调试时临时改成 2000 然后忘记改回来。日志行数一上去prompt_tokens 马上反弹而且 CrashLoopBackOff 的日志重复度很高加量对判断帮助有限。要排查更早的问题应该让用户自己用 kubectl logs 翻再把关键几行贴回对话。5.2 Base URL 多写 /v1 或 Key 没进环境变量如果调用返回 401 或通道错误先看两个地方。第一Base URL 是不是写成了 https://taotoken.net/api/v1正确写法是 https://taotoken.net/api末尾不加 /v1。第二TAOTOKEN_API_KEY 是否真的进了当前进程尤其是用 systemd、Docker Compose 或 K8s Deployment 部署时环境变量可能只写在本地 shell 里容器里根本没读到。确认 Key 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建并且没有多复制空格或换行。模型 ID 报错则多数是名字和模型广场对不上或者用了已经下线的 ID。把 TAOTOKEN_MODEL_ID 改成模型广场里当前可用的值重启诊断服务后再打一次 curl。不要把 401、404、超时三个问题混在一起排查先确认通道和 Key再确认模型 ID最后才看 Prompt 内容。6. 诊断结果回本地 kubectl 验证再去控制台对账6.1 模型只给只读命令执行仍在你的终端裁剪后的诊断输出仍然是建议不是执行结果。模型会根据 Pod 名字、阶段、上次退出码、最近 Warning 和日志尾部给出 kubectl 建议比如 kubectl describe pod、kubectl logs --previous、kubectl get events --field-selector involvedObject.name...。这些命令需要你在本地或跳板机上手动执行再把输出贴回对话做下一轮判断。诊断服务本身不连生产库也不替用户执行任何写操作这条边界首版必须守住。这样设计的好处是模型不需要拿到集群的写权限也不需要在 Prompt 里塞完整对象。你拿到的是一份「下一步查什么」的清单执行和确认仍然由工程师完成。等到这条链路稳定之后再考虑把常用只读命令的返回结果结构化地补进下一轮上下文而不是回到全量塞入的老路。6.2 对一下本次调用的 token 与套餐配置跑通后先在 TaoToken 模型对话 里用同一把 Key 发一条 CrashLoopBackOff 样例确认返回的是 kubectl 建议而不是通道报错然后在 控制台 API Keys 核对这把 Key 的调用记录看看 prompt_tokens 是否和 curl 里看到的一致。如果这个诊断助手要长期挂在内部平台可以打开 Coding Plan 看套餐是否覆盖调用量再决定要不要按环境拆 Key。首版到这里就算闭环client-go 负责把上下文砍到可解释的范围BuildPrompt 负责把 Runbook 说清楚TaoToken 的兼容通道负责让模型请求稳定出去curl 和 P95 负责验收。接下来要做的不是继续往 Prompt 里加数据而是把诊断记录和人工执行结果收回来用真实反馈调整那五个取数口。