基于提示词驱动的Function Call实现K8s Pod智能诊断

在云原生运维场景中,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 整体架构设计

平台采用"前端交互层-后端服务层-工具调用层-大模型交互层"四层架构,各层职责清晰、解耦良好:

  1. 前端交互层:提供下拉选择、诊断触发、流式结果展示,采用原生HTML+JS实现,无需额外框架;
  2. 后端服务层:基于FastAPI构建,提供命名空间/Pod列表查询、流式诊断接口等核心接口;
  3. 工具调用层:注册K8s相关工具(获取Pod状态、日志、事件),实现工具的自动解析与执行;
  4. 大模型交互层:通过提示词驱动大模型输出工具调用指令,解析指令并执行,最终获取诊断结果。

二、核心技术实现详解

"提示词驱动的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
相关推荐
修电脑的猫1 小时前
在中国使用 Claude Code 解决 403 错误(VS CODE)
ai·sap
大盛供应链·卢生2 小时前
哪些国产芯片可以替代进口芯片,哪些替代不了
fpga开发·容器
蚕豆糯米饭3 小时前
Github Copilot 研发效能提升实战指南
ai·github·copilot·ai编程
Token掘金室4 小时前
Aider配置自定义API教程
ai
Young丶6 小时前
讲透 Claude Code 系列 (四):Claude Skills 完全指南:可复用的“专业能力包”从入门到精通
人工智能·ai·ai编程·ai coding
小七-七牛开发者6 小时前
谷歌利用果蝇实现“AI 突围”?Cognition 再融 20 亿美元;AI 三巨头集体呼吁放慢脚步
ai·agent·token·skill·周一上线
Mr小林6 小时前
Docker 安装教程(在线 + 离线)
运维·docker·容器
阿里云云原生6 小时前
云栖剧透丨四场论坛,看清智能体走进生产的关键路径
云原生
子非鱼eva7 小时前
Ascend950PR版本速配表
人工智能·ai
Henry-SAP7 小时前
SAP PP模块核心机制解析
人工智能·云原生·sap·erp