构建基于GPU Ollama容器的安全AI Web应用

使用Flask + Ollama + NVD API构建本地CVE分析器

在本教程中,我们将利用已在GPU上运行的Ollama容器来构建一个Web应用程序。

这不仅仅是一个“向LLM提问的聊天机器人”,而是配置为从外部API获取实际数据,并让本地LLM分析这些数据。我们将构建的示例应用程序是一个本地CVE分析器。

当用户输入CVE ID或关键词时,Flask应用程序会从NVD API查询漏洞信息,然后Ollama上运行的本地LLM会用韩语总结和分析该漏洞。

Ollama的/api/chat端点可以接收model和messages作为输入,生成对话式响应,并且默认使用流式响应。在本教程中,为了方便Flask处理,我们使用stream: false。


1. 完成的应用程序结构

我们将构建的结构如下:

사용자 브라우저
    ↓
Flask Web UI
    ↓
Flask Backend
    ├── NVD CVE API 호출
    └── Ollama API 호출
            ↓
      로컬 GPU LLM 응답 생성
            ↓
사용자 화면에 CVE 원본 정보 + AI 분석 결과 출력

关键在于Flask服务器在中间扮演了两个角色。

首先,它使用用户输入的CVE ID或关键词调用NVD API。

其次,它将从NVD获取的漏洞信息传递给Ollama以获取分析结果。

在本教程中,我们不会让LLM直接调用API;相反,Flask会明确调用API,然后将结果传递给LLM。这样可以使初学者更容易理解流程,并减少实践中的错误。


2. 前提条件

本教程假设以下状态:

1. Ollama 컨테이너가 이미 실행 중이다.
2. Ollama 컨테이너가 GPU를 사용하도록 구성되어 있다.
3. Ollama API 포트 11434에 접근할 수 있다.
4. 사용할 모델이 이미 pull 되어 있거나, pull 할 수 있다.

例如,以下命令应在主机上正常运行:

curl http://localhost:11434/api/tags

响应示例如下:

{
  "models": [
    {
      "name": "llama3.1:latest"
    }
  ]
}

我们还会检查一个简单的聊天调用。

curl http://localhost:11434/api/chat 
  -H "Content-Type: application/json" 
  -d '{
    "model": "llama3.1",
    "messages": [
      {
        "role": "user",
        "content": "Ollama가 무엇인지 한 문장으로 설명해줘."
      }
    ],
    "stream": false
  }'

使用stream: false会将响应作为单个JSON对象返回,而不是分成多个片段,这使得在Flask应用程序中处理起来更容易。


3. 创建项目目录

首先,创建项目目录。

mkdir ollama-flask-cve-app
cd ollama-flask-cve-app

整体结构如下:

ollama-flask-cve-app/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.app.yml
├── templates/
│   └── index.html
└── static/
    └── style.css

Flask默认在templates目录中查找HTML模板,并使用render_template()函数渲染HTML页面。


4. 定义Python包

创建requirements.txt文件。

Flask==3.1.1
requests==2.32.4

Flask负责Web UI和路由,而requests用于调用Ollama API和NVD API。


5. 编写Flask后端

现在,我们来编写app.py。

import os
import re
from typing import Any, Dict, List, Optional

import requests
from flask import Flask, render_template, request

app = Flask(__name__)

# 如果Ollama容器在主机上通过11434端口暴露,则使用localhost
# 如果Flask应用也作为容器运行并连接到与Ollama容器相同的网络
# 例如:http://lab-ollama:11434/api/chat 或 http://ollama:11434/api/chat
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434/api/chat")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.1")

# NVD CVE API 2.0
NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"

# 可选:NVD API Key
# 如果有API Key,请将其指定为环境变量。
# export NVD_API_KEY="your-api-key"
NVD_API_KEY = os.getenv("NVD_API_KEY", "")

# 关键词搜索时获取的最大CVE数量
NVD_RESULTS_PER_PAGE = int(os.getenv("NVD_RESULTS_PER_PAGE", "5"))

def is_cve_id(keyword: str) -> bool:
    """
    입력값이 CVE ID 형식인지 확인합니다.
    예: CVE-2024-3094
    """
    return bool(re.fullmatch(r"CVE-d{4}-d{4,}", keyword.strip(), re.IGNORECASE))

def call_nvd_api(keyword: str) -> Dict[str, Any]:
    """
    NVD API에서 CVE 정보를 가져옵니다.

    CVE ID가 입력되면 cveIds 파라미터를 사용하고,
    일반 키워드가 입력되면 keywordSearch 파라미터를 사용합니다.
    """
    keyword = keyword.strip()

    if is_cve_id(keyword):
        params = {
            "cveIds": keyword.upper()
        }
    else:
        params = {
            "keywordSearch": keyword,
            "resultsPerPage": NVD_RESULTS_PER_PAGE
        }

    headers = {}

    if NVD_API_KEY:
        headers["apiKey"] = NVD_API_KEY

    response = requests.get(
        NVD_API_URL,
        params=params,
        headers=headers,
        timeout=30
    )

    response.raise_for_status()
    return response.json()

def get_english_description(cve: Dict[str, Any]) -> str:
    """
    CVE 설명 중 영어 설명을 우선 추출합니다.
    """
    descriptions = cve.get("descriptions", [])

    for item in descriptions:
        if item.get("lang") == "en":
            return item.get("value", "")

    if descriptions:
        return descriptions[0].get("value", "")

    return ""

def get_cvss_info(cve: Dict[str, Any]) -> Dict[str, str]:
    """
    CVSS 정보를 추출합니다.
    CVSS v4.0, v3.1, v3.0, v2 순서로 확인합니다.
    """
    metrics = cve.get("metrics", {})

    metric_order = [
        "cvssMetricV40",
        "cvssMetricV31",
        "cvssMetricV30",
        "cvssMetricV2",
    ]

    for metric_key in metric_order:
        metric_items = metrics.get(metric_key)

        if not metric_items:
            continue

        metric = metric_items[0]
        cvss_data = metric.get("cvssData", {})

        return {
            "version": str(cvss_data.get("version", metric_key)),
            "score": str(cvss_data.get("baseScore", "UNKNOWN")),
            "severity": str(
                metric.get("baseSeverity")
                or cvss_data.get("baseSeverity")
                or "UNKNOWN"
            ),
            "vector": str(cvss_data.get("vectorString", "UNKNOWN")),
        }

    return {
        "version": "UNKNOWN",
        "score": "UNKNOWN",
        "severity": "UNKNOWN",
        "vector": "UNKNOWN",
    }

def get_cwe_list(cve: Dict[str, Any]) -> List[str]:
    """
    CWE 목록을 추출합니다.
    """
    weaknesses = cve.get("weaknesses", [])
    cwe_list = []

    for weakness in weaknesses:
        for desc in weakness.get("description", []):
            value = desc.get("value")
            if value:
                cwe_list.append(value)

    return sorted(set(cwe_list))

def get_reference_urls(cve: Dict[str, Any], limit: int = 5) -> List[str]:
    """
    참고 URL을 일부만 추출합니다.
    """
    references = cve.get("references", {}).get("referenceData", [])
    urls = []

    for ref in references[:limit]:
        url = ref.get("url")
        if url:
            urls.append(url)

    return urls

def extract_cve_items(nvd_data: Dict[str, Any]) -> List[Dict[str, Any]]:
    """
    NVD API 응답에서 화면과 프롬프트에 사용할 정보만 추출합니다.
    """
    vulnerabilities = nvd_data.get("vulnerabilities", [])
    result = []

    for item in vulnerabilities:
        cve = item.get("cve", {})
        cvss = get_cvss_info(cve)

        result.append(
            {
                "id": cve.get("id", "UNKNOWN"),
                "published": cve.get("published", "UNKNOWN"),
                "last_modified": cve.get("lastModified", "UNKNOWN"),
                "status": cve.get("vulnStatus", "UNKNOWN"),
                "description": get_english_description(cve),
                "cvss_version": cvss["version"],
                "cvss_score": cvss["score"],
                "severity": cvss["severity"],
                "vector": cvss["vector"],
                "cwe": get_cwe_list(cve),
                "references": get_reference_urls(cve),
            }
        )

    return result

def format_cve_items_for_prompt(cve_items: List[Dict[str, Any]]) -> str:
    """
    Ollama에게 전달할 텍스트를 만듭니다.
    원본 JSON 전체를 넘기지 않고 필요한 필드만 전달합니다.
    """
    if not cve_items:
        return "검색 결과가 없습니다."

    lines = []

    for item in cve_items:
        cwe_text = ", ".join(item["cwe"]) if item["cwe"] else "UNKNOWN"
        ref_text = "
".join(item["references"]) if item["references"] else "NONE"

        lines.append(
            f"""
CVE ID: {item["id"]}
Published: {item["published"]}
Last Modified: {item["last_modified"]}
Status: {item["status"]}
Severity: {item["severity"]}
CVSS Version: {item["cvss_version"]}
CVSS Score: {item["cvss_score"]}
CVSS Vector: {item["vector"]}
CWE: {cwe_text}
Description:
{item["description"]}

References:
{ref_text}
"""
        )

    return "
---
".join(lines)

def ask_ollama(cve_text: str) -> str:
    """
    Ollama API를 호출해 CVE 분석 결과를 생성합니다.
    """
    prompt = f"""
너는 보안 분석가다.
아래 CVE 정보를 바탕으로 한국어로 분석해라.

반드시 지켜야 할 규칙:
1. 제공된 CVE 정보에 근거해서만 설명한다.
2. 원문에 없는 공격 절차를 과장해서 만들지 않는다.
3. 초보자도 이해할 수 있게 설명한다.
4. 운영자 관점의 대응 방안을 제시한다.
5. 출력은 다음 형식을 따른다.

출력 형식:
## 요약
## 취약점 설명
## 악용 가능성
## 위험도 판단
## 운영자 대응 방안
## 참고할 점

CVE 정보:
{cve_text}
"""

    payload = {
        "model": OLLAMA_MODEL,
        "messages": [
            {
                "role": "user",
                "content": prompt
            }
        ],
        "stream": False,
        "options": {
            "temperature": 0.2
        }
    }

    response = requests.post(
        OLLAMA_URL,
        json=payload,
        timeout=180
    )

    response.raise_for_status()
    data = response.json()

    return data.get("message", {}).get("content", "")

@app.route("/", methods=["GET", "POST"])
def index():
    """
    메인 페이지입니다.
    GET 요청이면 검색 폼만 보여주고,
    POST 요청이면 NVD API 조회 후 Ollama 분석 결과를 보여줍니다.
    """
    query = ""
    cve_items = []
    cve_prompt_text = ""
    analysis_result = ""
    error = ""

    if request.method == "POST":
        query = request.form.get("query", "").strip()

        if not query:
            error = "CVE ID 또는 검색 키워드를 입력하세요."
        else:
            try:
                nvd_data = call_nvd_api(query)
                cve_items = extract_cve_items(nvd_data)
                cve_prompt_text = format_cve_items_for_prompt(cve_items)

                if cve_items:
                    analysis_result = ask_ollama(cve_prompt_text)
                else:
                    error = "NVD API에서 검색 결과를 찾지 못했습니다."

            except requests.exceptions.HTTPError as e:
                error = f"HTTP 오류가 발생했습니다: {e}"
            except requests.exceptions.Timeout:
                error = "요청 시간이 초과되었습니다. 모델이 너무 크거나 API 응답이 느릴 수 있습니다."
            except requests.exceptions.RequestException as e:
                error = f"API 호출 중 오류가 발생했습니다: {e}"
            except Exception as e:
                error = f"처리 중 예상하지 못한 오류가 발생했습니다: {e}"

    return render_template(
        "index.html",
        query=query,
        model=OLLAMA_MODEL,
        ollama_url=OLLAMA_URL,
        cve_items=cve_items,
        cve_prompt_text=cve_prompt_text,
        analysis_result=analysis_result,
        error=error,
    )

if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port=5000,
        debug=True
    )

NVD CVE API 2.0的基本URL是。cveIds参数可用于单个CVE查询,keywordSearch可用于关键词搜索。resultsPerPage和startIndex用于结果分页。

此外,NVD API Key以apiKey:{key value}的形式在请求头中传递。如果没有API Key,限制为每30秒5次请求;如果有API Key,限制为每30秒50次请求。


6. 编写HTML UI

现在,创建templates/index.html文件。

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>Local CVE Analyst</title>

  <link
    rel="stylesheet"
    href="{{ url_for('static', filename='style.css') }}"
  />
</head>
<body>
  <main class="page">
    <section class="hero">
      <div class="badge">Ollama + Flask + NVD API</div>
      <h1>Local CVE Analyst</h1>
      <p>
        GPU 기반 Ollama 컨테이너와 NVD API를 연동해
        CVE 정보를 로컬 LLM으로 분석하는 보안 AI 웹앱입니다.
      </p>
    </section>

    <section class="panel">
      <form method="POST" class="search-form">
        <label for="query">CVE ID 또는 검색 키워드</label>

        <div class="input-row">
          <input
            id="query"
            name="query"
            type="text"
            value="{{ query }}"
            placeholder="예: CVE-2024-3094 또는 openssl"
            autocomplete="off"
          />

          <button type="submit">
            분석하기
          </button>
        </div>
      </form>

      <div class="runtime-info">
        <span>Model: {{ model }}</span>
        <span>Ollama API: {{ ollama_url }}</span>
      </div>
    </section>

    {% if error %}
    <section class="alert">
      {{ error }}
    </section>
    {% endif %}

    {% if cve_items %}
    <section class="grid">
      <div class="panel">
        <h2>NVD 검색 결과</h2>

        {% for item in cve_items %}
        <article class="cve-card">
          <div class="cve-header">
            <h3>{{ item.id }}</h3>
            <span class="severity">{{ item.severity }}</span>
          </div>

          <dl>
            <div>
              <dt>CVSS</dt>
              <dd>{{ item.cvss_score }} / {{ item.cvss_version }}</dd>
            </div>
            <div>
              <dt>Status</dt>
              <dd>{{ item.status }}</dd>
            </div>
            <div>
              <dt>Published</dt>
              <dd>{{ item.published }}</dd>
            </div>
            <div>
              <dt>Last Modified</dt>
              <dd>{{ item.last_modified }}</dd>
            </div>
          </dl>

          <p class="description">
            {{ item.description }}
          </p>

          {% if item.cwe %}
          <div class="chips">
            {% for cwe in item.cwe %}
            <span>{{ cwe }}</span>
            {% endfor %}
          </div>
          {% endif %}
        </article>
        {% endfor %}
      </div>

      <div class="panel">
        <h2>Ollama 분석 결과</h2>

        {% if analysis_result %}
        <pre class="analysis">{{ analysis_result }}</pre>
        {% else %}
        <p class="muted">분석 결과가 없습니다.</p>
        {% endif %}
      </div>
    </section>

    <section class="panel">
      <h2>LLM에 전달한 요약 데이터</h2>
      <pre class="raw">{{ cve_prompt_text }}</pre>
    </section>
    {% endif %}
  </main>
</body>
</html>

这里重要的一点是analysis_result没有进行安全处理。将LLM响应或外部API响应作为HTML信任可能会导致XSS问题。因此,我们使用

标签和CSS的white-space处理来自然地显示换行符。


7. 编写CSS

创建static/style.css文件。

:root {
  --bg: #f3f4f6;
  --panel: #ffffff;
  --text: #111827;
  --muted: #6b7280;
  --border: #e5e7eb;
  --primary: #111827;
  --primary-hover: #374151;
  --danger-bg: #fee2e2;
  --danger-text: #991b1b;
  --code-bg: #0f172a;
  --code-text: #e5e7eb;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family:
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    "Noto Sans KR",
    Arial,
    sans-serif;
  background: var(--bg);
  color: var(--text);
}

.page {
  width: 1120px;
  max-width: 94%;
  margin: 40px auto;
}

.hero {
  margin-bottom: 24px;
}

.badge {
  display: inline-block;
  padding: 6px 10px;
  margin-bottom: 12px;
  border-radius: 999px;
  background: #e5e7eb;
  color: #374151;
  font-size: 13px;
  font-weight: 700;
}

.hero h1 {
  margin: 0 0 12px;
  font-size: 42px;
  letter-spacing: -0.04em;
}

.hero p {
  margin: 0;
  color: var(--muted);
  font-size: 18px;
  line-height: 1.6;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 24px;
  margin-bottom: 20px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}

.search-form label {
  display: block;
  margin-bottom: 10px;
  font-weight: 800;
}

.input-row {
  display: flex;
  gap: 10px;
}

.input-row input {
  flex: 1;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 16px;
}

.input-row button {
  padding: 0 22px;
  border: none;
  border-radius: 12px;
  background: var(--primary);
  color: white;
  font-size: 16px;
  font-weight: 800;
  cursor: pointer;
}

.input-row button:hover {
  background: var(--primary-hover);
}

.runtime-info {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
  color: var(--muted);
  font-size: 13px;
}

.runtime-info span {
  padding: 6px 10px;
  background: #f9fafb;
  border: 1px solid var(--border);
  border-radius: 999px;
}

.alert {
  padding: 16px;
  margin-bottom: 20px;
  border-radius: 14px;
  background: var(--danger-bg);
  color: var(--danger-text);
  font-weight: 700;
}

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  align-items: start;
}

h2 {
  margin-top: 0;
  margin-bottom: 16px;
  font-size: 22px;
}

.cve-card {
  padding: 18px;
  border: 1px solid var(--border);
  border-radius: 14px;
  margin-bottom: 14px;
  background: #f9fafb;
}

.cve-header {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  align-items: center;
  margin-bottom: 12px;
}

.cve-header h3 {
  margin: 0;
}

.severity {
  padding: 5px 9px;
  border-radius: 999px;
  background: #111827;
  color: #ffffff;
  font-size: 12px;
  font-weight: 800;
}

dl {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin: 0 0 14px;
}

dt {
  color: var(--muted);
  font-size: 12px;
}

dd {
  margin: 2px 0 0;
  font-weight: 700;
  font-size: 14px;
}

.description {
  line-height: 1.6;
  color: #374151;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chips span {
  padding: 5px 8px;
  border-radius: 999px;
  background: #e5e7eb;
  color: #374151;
  font-size: 12px;
  font-weight: 700;
}

pre {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.6;
}

.analysis {
  min-height: 300px;
  color: var(--text);
  font-family: inherit;
}

.raw {
  padding: 16px;
  border-radius: 12px;
  background: var(--code-bg);
  color: var(--code-text);
  font-size: 13px;
}

.muted {
  color: var(--muted);
}

@media (max-width: 900px) {
  .grid {
    grid-template-columns: 1fr;
  }

  .input-row {
    flex-direction: column;
  }

  .input-row button {
    padding: 14px 22px;
  }

  .hero h1 {
    font-size: 34px;
  }
}

8. 本地运行

安装软件包。

pip install -r requirements.txt

指定Ollama API URL和模型名称。

export OLLAMA_URL="http://localhost:11434/api/chat"
export OLLAMA_MODEL="llama3.1"

如果拥有NVD API Key,可以选择性地指定。

export NVD_API_KEY="your-nvd-api-key"

运行Flask应用程序。

python app.py

在浏览器中访问以下地址:

http://localhost:5000

尝试在搜索栏中输入以下内容:

CVE-2024-3094

或者您也可以按关键词搜索:

openssl

屏幕上会显示三个主要信息:

1. NVD API에서 가져온 CVE 기본 정보
2. Ollama가 생성한 한국어 취약점 분석
3. Ollama에게 실제로 전달한 요약 데이터

9. 使用容器运行

如果您想将Flask应用程序也作为容器运行,请创建Dockerfile。

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

构建应用程序镜像。

docker build -t ollama-flask-cve-app:latest .

如果Ollama容器在主机上暴露了11434端口,Docker在某些情况下可以使用以下地址:

http://host.docker.internal:11434/api/chat

然而,在Linux服务器或实践环境中,host.docker.internal可能无法始终保证。最简洁的方法是将Ollama容器和Flask应用程序容器放在同一个Docker网络中。

例如,假设Ollama容器的名称是lab-ollama。

docker network create ollama-net
docker network connect ollama-net lab-ollama

现在,将Flask应用程序容器连接到同一个网络。

docker run --rm -it 
  --name local-cve-analyst 
  --network ollama-net 
  -p 5000:5000 
  -e OLLAMA_URL="http://lab-ollama:11434/api/chat" 
  -e OLLAMA_MODEL="llama3.1" 
  ollama-flask-cve-app:latest

现在,在浏览器中访问:

http://localhost:5000

10. Docker Compose示例

如果Ollama容器已经单独运行,您也可以只用Compose管理应用程序。

创建docker-compose.app.yml文件。

services:
  cve-app:
    build: .
    container_name: local-cve-analyst
    ports:
      - "5000:5000"
    environment:
      OLLAMA_URL: "http://lab-ollama:11434/api/chat"
      OLLAMA_MODEL: "llama3.1"
      NVD_RESULTS_PER_PAGE: "5"
      # NVD_API_KEY: "your-nvd-api-key"
    networks:
      - ollama-net

networks:
  ollama-net:
    external: true

运行它。

docker compose -f docker-compose.app.yml up --build

如果Ollama容器的名称是ollama,您可以按如下方式更改:

OLLAMA_URL: "http://ollama:11434/api/chat"

重要的一点是,当Flask应用程序在容器内部运行时,localhost指的是Flask应用程序容器本身,而不是Ollama容器。因此,要从应用程序容器访问Ollama容器,最容易理解的方法是将其连接到同一个网络,并使用容器名称作为主机名。


11. 在Podman环境中运行

如果您的实践脚本基于Podman,也可以类似地进行配置。

假设Ollama容器以lab-ollama的名称运行。

podman network create ollama-net
podman network connect ollama-net lab-ollama

构建镜像。

podman build -t ollama-flask-cve-app:latest .

运行应用程序容器。

podman run --rm -it 
  --name local-cve-analyst 
  --network ollama-net 
  -p 5000:5000 
  -e OLLAMA_URL="http://lab-ollama:11434/api/chat" 
  -e OLLAMA_MODEL="llama3.1" 
  ollama-flask-cve-app:latest

在浏览器中访问以下地址:

http://localhost:5000

12. 测试场景

第一个测试是单个CVE ID查询。

CVE-2024-3094

在这种情况下,Flask应用程序会以cveIds=CVE-2024-3094的形式向NVD API发出请求。

第二个测试是关键词搜索。

openssl

在这种情况下,Flask应用程序会以keywordSearch=openssl的形式向NVD API发出请求。

第三个测试是针对不存在的CVE ID。

CVE-2099-0000

在这种情况下,应该显示一条消息,指示没有搜索结果。


13. 常见错误

13.1 Ollama API连接失败

错误示例如下:

Connection refused

需要检查的内容如下:

curl http://localhost:11434/api/tags

如果主机上可以,但在应用程序容器内部不行,则OLLAMA_URL很可能不正确。

如果在本地运行Flask应用程序,请使用以下内容:

export OLLAMA_URL="http://localhost:11434/api/chat"

如果在容器中运行Flask应用程序,并且Ollama容器的名称是lab-ollama,请使用以下内容:

export OLLAMA_URL="http://lab-ollama:11434/api/chat"

13.2 模型不存在错误

如果Ollama响应显示模型不存在,您必须首先拉取该模型。

ollama pull llama3.1

如果Ollama在容器内部运行,请按如下方式运行:

docker exec -it lab-ollama ollama pull llama3.1

或者,在Podman环境中,如下所示:

podman exec -it lab-ollama ollama pull llama3.1

13.3 NVD API 403或限制错误

NVD API即使对于公共访问也有速率限制。如果没有API Key,限制为每30秒5次请求;如果有API Key,限制为每30秒50次请求。

在实践中,多次点击按钮可能会触发限制。在这种情况下,请稍等片刻再试。

为了使其达到生产级别,建议添加以下功能:

1. NVD API 응답 캐싱
2. 동일 검색어 반복 요청 제한
3. API Key 사용
4. 사용자별 요청 제한

13.4 响应时间过长

如果本地LLM模型较大或GPU内存不足,响应时间可能会很长。

在这种情况下,请调整以下内容:

1. 더 작은 모델 사용
2. NVD_RESULTS_PER_PAGE 값을 1~3으로 축소
3. 프롬프트 길이 축소
4. Ollama keep_alive 설정 조정

例如,将结果数量减少到3个。

export NVD_RESULTS_PER_PAGE="3"

14. 此应用程序的学习要点

本次实践的核心是“LLM应用程序不仅仅是附加一个聊天窗口”。

此应用程序包含以下所有概念:

1. 웹 UI
2. Flask 백엔드
3. 외부 REST API 연동
4. 로컬 LLM API 호출
5. 프롬프트 구성
6. 컨테이너 네트워크
7. 보안 데이터 분석

这种结构在安全教育中特别有用。它不仅仅是简单地显示CVE、CWE、CVSS等安全数据,而是让LLM从操作员的角度将其转换为解释。


15. 下一步:扩展到工具调用结构

在本教程中,Flask首先调用NVD API,然后将结果传递给Ollama。

사용자 입력
    ↓
Flask가 NVD API 호출
    ↓
Flask가 Ollama 호출
    ↓
최종 답변 출력

在高级版本中,您可以使用Ollama的工具调用功能。Ollama支持模型调用工具并将其结果反映到响应中的工具调用功能。

高级结构如下:

사용자 입력
    ↓
Ollama가 필요한 도구 판단
    ↓
Flask가 해당 도구 함수 실행
    ↓
도구 실행 결과를 Ollama에 다시 전달
    ↓
최종 답변 출력

然而,如果一开始就采用这种结构,解释会变得复杂。因此,对于入门教程来说,像现在这样“Flask调用外部API,Ollama只负责分析的结构”更合适。


16. 总结

在本教程中,我们假设基于GPU的Ollama容器已经准备就绪,并使用Flask创建了一个具有实际Web UI的本地LLM应用程序。

完成的应用程序按以下流程运行:

CVE ID 또는 키워드 입력
    ↓
NVD API에서 취약점 정보 조회
    ↓
필요한 필드만 추출
    ↓
Ollama 로컬 LLM에 분석 요청
    ↓
웹 화면에 원본 정보와 AI 분석 결과 출력

这个示例比简单的聊天机器人具有更高的实践价值。因为它展示了连接外部API和本地LLM的基本模式。应用此模式,可以扩展到GitHub仓库分析器、日志分析助手、安全检查报告生成器、内部文档搜索型RAG应用程序等。


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注