在云原生运维场景中,K8s Pod故障诊断是日常工作的核心痛点之一。传统诊断方式依赖运维人员手动执行kubectl命令、分析日志和事件,效率低下且对专业能力要求较高。随着大模型技术的发展,Function Call(工具调用)为自动化故障诊断提供了全新思路------让大模型自主决策、调用工具、分析结果,最终输出专业诊断报告。
本文将详细介绍如何基于提示词驱动的Function Call模式,构建一个可落地、高兼容、交互友好的K8s Pod智能诊断平台,涵盖从功能设计、技术实现到前端交互的完整流程,同时解决实际落地中的兼容性、异常处理等关键问题。
一、核心功能架构
核心目标是:构建一个无需手动输入命令、无需原生Function Call支持,即可兼容各类大模型(如Ollama、OpenRouter等)的K8s Pod智能诊断系统。区别于传统运维工具,该平台通过大模型的自主决策能力,实现故障诊断的全流程自动化,降低运维门槛。
1.1 核心功能清单
命名空间(Namespace)与Pod名称下拉选择,无需手动输入,避免输入错误;
- 一键诊断触发,按钮交互优化(点击变灰、诊断中提示,避免重复点击);
- 流式输出诊断结果,模拟ChatGPT逐字打字效果,提升用户体验;
- 自动调用K8s工具(获取Pod状态、日志、事件),无需人工干预;
- 异常优雅处理(模型调用失败、K8s连接异常等),避免系统崩溃;
- 输出专业诊断报告,包含故障状态、根因分析、修复方案及验证命令。
1.2 整体架构设计
平台采用"前端交互层-后端服务层-工具调用层-大模型交互层"四层架构,各层职责清晰、解耦良好:
- 前端交互层:提供下拉选择、诊断触发、流式结果展示,采用原生HTML+JS实现,无需额外框架;
- 后端服务层:基于FastAPI构建,提供命名空间/Pod列表查询、流式诊断接口等核心接口;
- 工具调用层:注册K8s相关工具(获取Pod状态、日志、事件),实现工具的自动解析与执行;
- 大模型交互层:通过提示词驱动大模型输出工具调用指令,解析指令并执行,最终获取诊断结果。
二、核心技术实现详解
"提示词驱动的Function Call"------不依赖大模型原生的Function Call支持(如OpenAI的tools参数),通过自定义提示词规范大模型输出格式,再通过代码解析输出结果,实现工具调用。这种方式的优势在于兼容性极强,可适配所有支持文本输出的大模型(云端Ollama、OpenRouter、通义千问等)。
2.1 提示词设计:规范大模型工具调用行为
提示词是Function Call的核心,需明确告诉大模型"何时调用工具、如何调用工具、输出什么格式"。本项目的提示词设计如下,兼顾简洁性和严谨性:
python
TOOL_PROMPT = """
你是K8s高级故障诊断专家。
规则:
1. 每次只返回1个工具调用JSON
2. 格式:{"tool":"工具名","params":{"pod_name":"xxx","namespace":"xxx"}}
3. 禁止一次性返回多个JSON!
工具:
get_pod_state(pod_name,namespace)
get_pod_logs(pod_name,namespace)
describe_pod_events(pod_name,namespace)
获取信息后,直接输出诊断:状态、根因、修复方案。
实际诊断效果示例
python
{"tool":"get_pod_state","params":{"pod_name":"errtestnginx-69bf5d5f96-qf65f","namespace":"default"}}{"tool":"describe_pod_events","params":{"pod_name":"errtestnginx-69bf5d5f96-qf65f","namespace":"default"}}诊断报告
**Pod状态:** Pending (ImagePullBackOff)
**根因分析:**
镜像拉取失败。容器尝试拉取 `nginx:test` 镜像时被拒绝(403 Forbidden),表明镜像仓库无法解析该标签。可能原因:
- 镜像标签 `test` 不存在或拼写错误
- 私有仓库缺少认证(imagePullSecrets)
- 镜像代理(docker.m.daocloud.io)配置错误或访问受限
**修复方案:**
1. **修正镜像标签**(最可能):将 `nginx:test` 改为有效标签,如 `nginx:latest` 或 `nginx:1.25`
2. **检查仓库认证**:如为私有仓库,确认已配置正确的 `imagePullSecrets`
3. **验证代理配置**:检查 `/etc/containerd/config.toml` 或 CRI 配置中的镜像加速地址是否可用
4. **手动验证**:在节点执行 `crictl pull nginx:latest` 测试连通性
建议优先执行方案1修改 Deployment 镜像标签。

代码:
python
# -*- coding: utf-8 -*-
import json
import logging
import os
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse
from openai import OpenAI
from kubernetes import client, config
# ===================== 配置 =====================
load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
openai_client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("API_BASE_URL")
)
MODEL = os.getenv("LLM_MODEL")
# K8s
k8s_core_api = None
try:
config.load_kube_config()
k8s_core_api = client.CoreV1Api()
logger.info("✅ K8s 连接成功")
except Exception as e:
logger.error(f"❌ K8s 连接失败: {e}")
app = FastAPI(title="K8s 智能诊断平台")
# ===================== 工具注册 =====================
TOOL_REGISTRY = {}
def register_tool(func):
TOOL_REGISTRY[func.__name__] = func
return func
@register_tool
def get_pod_state(pod_name, namespace="default"):
try:
pod = k8s_core_api.read_namespaced_pod(pod_name, namespace)
return f"状态={pod.status.phase}, 容器状态={pod.status.container_statuses}"
except Exception as e:
return "获取状态失败: " + str(e)
@register_tool
def get_pod_logs(pod_name, namespace="default"):
try:
return k8s_core_api.read_namespaced_pod_log(pod_name, namespace, tail_lines=80)
except Exception as e:
return "获取日志失败: " + str(e)
@register_tool
def describe_pod_events(pod_name, namespace="default"):
try:
events = k8s_core_api.list_namespaced_event(
namespace, field_selector=f"involvedObject.name={pod_name}"
)
lines = []
for e in events.items[-10:]:
lines.append(f"{e.type} {e.reason}: {e.message}")
return "\n".join(lines)
except Exception as e:
return "获取事件失败: " + str(e)
# ===================== 接口:获取命名空间 & Pod =====================
@app.get("/api/namespaces")
def get_namespaces():
try:
ret = k8s_core_api.list_namespace()
return [ns.metadata.name for ns in ret.items]
except:
return ["default"]
@app.get("/api/pods")
def get_pods(namespace: str):
try:
ret = k8s_core_api.list_namespaced_pod(namespace)
return [p.metadata.name for p in ret.items]
except:
return []
# ===================== 提示词 =====================
TOOL_PROMPT = """
你是K8s高级故障诊断专家。
规则:
1. 每次只返回1个工具调用JSON
2. 格式:{"tool":"工具名","params":{"pod_name":"xxx","namespace":"xxx"}}
3. 禁止一次性返回多个JSON!
工具:
get_pod_state(pod_name,namespace)
get_pod_logs(pod_name,namespace)
describe_pod_events(pod_name,namespace)
获取信息后,直接输出诊断:状态、根因、修复方案。
"""
# ===================== 解析工具 =====================
def parse_and_run_tools(reply, messages):
try:
parts = reply.strip().split("\n")
for part in parts:
part = part.strip()
if not part: continue
tool_data = json.loads(part)
tool_name = tool_data.get("tool")
params = tool_data.get("params", {})
logger.info(f"✅ 执行工具: {tool_name} {params}")
tool_result = TOOL_REGISTRY[tool_name](**params)
messages.append({"role": "assistant", "content": part})
messages.append({"role": "user", "content": f"工具结果:\n{tool_result}"})
return True
except Exception as e:
logger.info(f"工具解析失败: {e}")
return False
# ===================== 流式输出 =====================
async def stream_diagnose(pod_name, namespace):
messages = [
{"role": "system", "content": TOOL_PROMPT},
{"role": "user", "content": f"诊断Pod异常: {namespace}/{pod_name}"}
]
for _ in range(5):
response = openai_client.chat.completions.create(
model=MODEL,
messages=messages,
stream=True
)
full_reply = ""
for chunk in response:
token = chunk.choices[0].delta.content
if token:
full_reply += token
yield f"data: {json.dumps({'token': token})}\n\n"
if parse_and_run_tools(full_reply, messages):
continue
yield f"data: [DONE]\n\n"
return
yield f"data: {json.dumps({'error': '诊断失败'})}\n\n"
yield f"data: [DONE]\n\n"
# ===================== API =====================
@app.get("/api/debug/stream")
async def api_debug_stream(pod_name: str, namespace: str = "default"):
return StreamingResponse(
stream_diagnose(pod_name, namespace),
media_type="text/event-stream"
)
# ===================== 前端 =====================
@app.get("/", response_class=HTMLResponse)
def index():
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>K8s智能诊断</title>
<style>
body{background:#0f172a;color:#e2e8f0;padding:2rem;font-family:system-ui}
.card{max-width:700px;margin:auto;background:#1e293b;padding:24px;border-radius:12px}
select,button{
width:100%;padding:12px;margin:10px 0;
background:#334155;color:white;border:0;border-radius:8px;font-size:15px
}
button{background:#2563eb;font-weight:bold;cursor:pointer}
button:disabled{
background:#475569;
cursor:not-allowed;
opacity:0.7;
}
#result{
background:#111827;padding:16px;border-radius:8px;margin-top:16px;
white-space:pre-wrap;line-height:1.6;min-height:60px
}
</style>
</head>
<body>
<div class="card">
<h2> K8s Pod 自动诊断</h2>
<label>命名空间</label>
<select id="ns"></select>
<label>Pod 名称</label>
<select id="pod"></select>
<button id="diagnoseBtn" onclick="startDiagnose()">开始诊断</button>
<div id="result"></div>
</div>
<script>
// 加载命名空间
async function loadNamespaces(){
const r = await fetch("/api/namespaces");
const list = await r.json();
const sel = document.getElementById("ns");
sel.innerHTML = "";
list.forEach(n=>{
const o = document.createElement("option");
o.value=n; o.innerText=n; sel.appendChild(o);
});
loadPods();
}
// 加载Pod
async function loadPods(){
const ns = document.getElementById("ns").value;
const r = await fetch("/api/pods?namespace="+ns);
const list = await r.json();
const sel = document.getElementById("pod");
sel.innerHTML = "";
list.forEach(p=>{
const o = document.createElement("option");
o.value=p; o.innerText=p; sel.appendChild(o);
});
}
// 流式诊断(按钮变灰 + 流畅交互)
async function startDiagnose(){
const btn = document.getElementById('diagnoseBtn');
const res = document.getElementById('result');
const ns = document.getElementById("ns").value;
const pod = document.getElementById("pod").value;
// 按钮禁用 + 变灰 + 文字修改
btn.disabled = true;
btn.innerText = "诊断中,请稍候...";
res.innerText = "";
try {
const resp = await fetch(`/api/debug/stream?namespace=${ns}&pod_name=${pod}`);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while(true){
const {done,value} = await reader.read();
if(done) break;
const text = decoder.decode(value);
text.split("\\n").forEach(line=>{
if(line.startsWith("data: ")){
const data = line.slice(6);
if(data === "[DONE]") return;
try{
const obj = JSON.parse(data);
if(obj.token) res.innerText += obj.token;
}catch(e){}
}
});
}
} finally {
// 恢复按钮
btn.disabled = false;
btn.innerText = "开始诊断";
}
}
document.getElementById("ns").onchange = loadPods;
loadNamespaces();
</script>
</body>
</html>
"""
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
安装依赖
powershell
pip install fastapi uvicorn python-dotenv openai kubernetes
配置环境变量(.env文件)
python
OPENAI_API_KEY=apikey
API_BASE_URL=https://ollama.com/v1
LLM_MODEL=kimi-k2.5
# 切换模型模式
# chat = /v1/chat/completions(默认)
# completions = /v1/completions
MODEL_MODE=chat