文章目录
-
- 一、为什么要做这个工具
- 二、项目结构和处理流程
- 三、蓝耘元生代在项目中承担什么
- [四、准备蓝耘 MaaS 配置](#四、准备蓝耘 MaaS 配置)
-
- [1. 创建项目专用 API Key](#1. 创建项目专用 API Key)
- [2. 从模型列表复制完整 ID](#2. 从模型列表复制完整 ID)
- [3. 配置环境变量](#3. 配置环境变量)
- 五、完整代码
-
- [1. 安装依赖](#1. 安装依赖)
- [2. 日志预处理和模型调用](#2. 日志预处理和模型调用)
- [3. FastAPI 接口](#3. FastAPI 接口)
- 六、用一段故障日志验证
- [七、一个实际会碰到的问题:模型返回 JSON,但程序仍然解析失败](#七、一个实际会碰到的问题:模型返回 JSON,但程序仍然解析失败)
- 八、另一个容易忽略的问题:日志脱敏不能只靠提示词
- 九、如何部署
- 十、模型怎么分工更合理
- 十一、这次实战得到的结论
这篇文章记录一个可以直接运行的小项目:接收 FastAPI、Nginx 等服务的错误日志,通过蓝耘元生代 MaaS 调用不同模型,生成结构化的故障摘要、根因判断和排查步骤。重点不是做一个"会聊天"的页面,而是把值班时最费时间的日志整理工作先交给模型。
一、为什么要做这个工具
我把目标场景定在一个很常见的线上故障:订单接口间歇性返回 502,监控里同时出现 Nginx upstream timeout、Python 数据库连接超时和 Redis 重连日志。类似事故往往会在十几分钟内积累几百行重复信息。
真正有用的信息其实不多:故障从什么时候开始、哪个服务最先异常、错误有没有固定请求路径、应该先查数据库还是网关。但人刚被告警叫起来时,很容易顺着最后一条错误往下查,结果在次要问题上绕一圈。本文使用后面给出的脱敏模拟日志复现这条链路,不把示例数据冒充真实生产事故。
我想做的工具很克制,只完成四件事:
- 从日志中提取时间范围、服务名和关键错误;
- 把重复堆栈折叠成故障摘要;
- 给出有证据支撑的可能根因,不允许把猜测写成结论;
- 生成按顺序执行的排查清单。
这个任务适合接 MaaS。日志分析请求通常不连续,凌晨可能突然来一批,平时又几乎没有调用。为了这种波峰波谷自己维护一套 GPU 推理服务并不划算。
二、项目结构和处理流程
项目使用 FastAPI 提供接口,Python 负责日志预处理,蓝耘元生代负责语义分析。流程如下:
text
原始日志
↓
大小与格式检查
↓
正则提取时间、级别、服务名
↓
相同错误去重并保留出现次数
↓
根据日志长度选择模型
↓
调用蓝耘 OpenAI 兼容接口
↓
校验 JSON 输出
↓
返回故障摘要、证据、根因候选和排查步骤
我没有把完整原始日志不加处理地扔给模型。预处理可以减少重复 Token,也能保留"某错误出现 37 次"这种频次信息。模型擅长理解上下文,但基础清洗还是普通代码更稳。
本项目目录很小:
text
log-doctor/
├── app.py
├── analyzer.py
├── sample.log
├── requirements.txt
└── .env.example
三、蓝耘元生代在项目中承担什么
蓝耘在这里是模型服务层。FastAPI 处理 HTTP 请求,预处理代码压缩日志,蓝耘 MaaS 接收整理后的文本并返回分析结果。
我选择蓝耘主要有三个实际原因:
- 接口兼容 OpenAI SDK,现有 Python 项目只需修改
base_url、API Key 和模型 ID; - 同一入口可以配置不同模型,短日志用响应快的模型,长堆栈换成长文本能力更合适的模型;
- 模型和密钥集中在平台侧管理,不需要为偶发的日志分析任务维护推理服务。
几种方案各有适用范围:
| 方案 | 初次接入 | 日常运维 | 数据边界 | 适合场景 |
|---|---|---|---|---|
| 本地部署开源模型 | 较高 | 需要维护显存、服务和模型文件 | 数据可留在内网 | 日志不能出内网、调用长期稳定 |
| 直接连接单一模型厂商 | 较低 | 较少 | 取决于服务协议 | 团队已固定使用某一模型 |
| 蓝耘元生代 MaaS | 较低 | 较少 | 调用前仍需自行脱敏 | 希望统一接口并灵活选择模型 |
这不是说 MaaS 一定优于本地部署。涉及密码、Token、手机号、内网地址的日志,必须先脱敏;如果公司规定日志不能离开内网,就应选择本地模型。
四、准备蓝耘 MaaS 配置
1. 创建项目专用 API Key
进入蓝耘元生代 MaaS 控制台,在 API Key 管理页创建一枚只给日志助手使用的密钥。不要复用个人测试 Key,也不要把密钥直接写进代码仓库。

图 1:蓝耘 MaaS API Key 管理操作示意。实际发布时建议替换成自己的控制台截图,并遮挡完整密钥。
OpenAI 兼容接口地址为:
text
https://maas-api.lanyun.net/v1
2. 从模型列表复制完整 ID
模型 ID 不要凭展示名称手写。蓝耘模型通常带有完整命名空间,例如:
text
/maas/deepseek-ai/DeepSeek-V3.2
模型上下架和命名可能调整,运行时应以本人控制台当前显示为准。

图 2:蓝耘 MaaS 模型列表操作示意。需要记录完整模型 ID,而不是只抄页面上的短名称。
3. 配置环境变量
.env.example 内容如下:
dotenv
LANYUN_API_KEY=替换为自己的API_KEY
LANYUN_BASE_URL=https://maas-api.lanyun.net/v1
LANYUN_FAST_MODEL=/maas/deepseek-ai/DeepSeek-V3.2
LANYUN_LONG_MODEL=/maas/deepseek-ai/DeepSeek-V3.2
LOG_MAX_CHARS=60000
为了让示例可以直接跑,两个任务先使用同一个模型。实际项目中可以把 LANYUN_FAST_MODEL 换成更轻量的模型,把 LANYUN_LONG_MODEL 配成长上下文模型,业务代码不用改。
五、完整代码
1. 安装依赖
requirements.txt:
text
fastapi==0.115.12
uvicorn[standard]==0.34.2
openai==1.78.1
python-dotenv==1.1.0
pydantic==2.11.4
安装:
bash
python -m venv .venv
Windows PowerShell:
powershell
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
Linux 或 macOS:
bash
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
2. 日志预处理和模型调用
analyzer.py:
python
import hashlib
import json
import os
import re
from collections import Counter
from typing import Any
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
BASE_URL = os.getenv("LANYUN_BASE_URL", "https://maas-api.lanyun.net/v1")
FAST_MODEL = os.environ["LANYUN_FAST_MODEL"]
LONG_MODEL = os.environ["LANYUN_LONG_MODEL"]
MAX_CHARS = int(os.getenv("LOG_MAX_CHARS", "60000"))
client = OpenAI(
api_key=os.environ["LANYUN_API_KEY"],
base_url=BASE_URL,
timeout=90.0,
)
TIME_PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?")
LEVEL_PATTERN = re.compile(r"\b(DEBUG|INFO|WARN|WARNING|ERROR|CRITICAL)\b", re.I)
SECRET_PATTERNS = [
(re.compile(r"(?i)(authorization:\s*bearer\s+)[^\s]+"), r"\1***"),
(re.compile(r"(?i)(api[_-]?key[=:]\s*)[^\s,;]+"), r"\1***"),
(re.compile(r"(?i)(password[=:]\s*)[^\s,;]+"), r"\1***"),
(re.compile(r"\b1[3-9]\d{9}\b"), "[PHONE]"),
]
SYSTEM_PROMPT = """
你是生产环境日志分析助手。只根据提供的日志作答,不补造系统信息。
请输出一个 JSON 对象,字段必须为:
summary:不超过80字的故障摘要;
time_range:日志覆盖时间,无法判断时写 unknown;
services:涉及的服务名数组;
key_errors:关键错误数组,每项包含 message、count、evidence;
root_causes:根因候选数组,每项包含 cause、confidence、evidence;
actions:按优先级排列的排查步骤数组;
missing_context:仍需补充的信息数组。
confidence 只能是 high、medium、low。
证据不足时必须降低置信度,禁止把猜测写成确定事实。
不要输出 Markdown,不要在 JSON 前后添加解释。
""".strip()
def redact_secrets(text: str) -> str:
for pattern, replacement in SECRET_PATTERNS:
text = pattern.sub(replacement, text)
return text
def normalize_line(line: str) -> str:
line = TIME_PATTERN.sub("<TIME>", line)
line = re.sub(r"request_id=[a-zA-Z0-9-]+", "request_id=<ID>", line)
line = re.sub(r"\b\d+ms\b", "<N>ms", line)
return line.strip()
def compress_log(raw_log: str) -> dict[str, Any]:
safe_log = redact_secrets(raw_log[:MAX_CHARS])
lines = [line.strip() for line in safe_log.splitlines() if line.strip()]
normalized = [normalize_line(line) for line in lines]
counts = Counter(normalized)
important = []
for original, normalized_line in zip(lines, normalized):
level = LEVEL_PATTERN.search(original)
if level and level.group(1).upper() in {"WARN", "WARNING", "ERROR", "CRITICAL"}:
important.append(
{
"line": original[:500],
"count": counts[normalized_line],
}
)
unique_important = []
seen = set()
for item in important:
key = normalize_line(item["line"])
if key not in seen:
seen.add(key)
unique_important.append(item)
times = TIME_PATTERN.findall(safe_log)
return {
"sha256": hashlib.sha256(safe_log.encode("utf-8")).hexdigest(),
"original_lines": len(lines),
"time_start": times[0] if times else "unknown",
"time_end": times[-1] if times else "unknown",
"important_lines": unique_important[:120],
"truncated": len(raw_log) > MAX_CHARS,
}
def choose_model(payload: dict[str, Any]) -> str:
if payload["original_lines"] > 300 or len(payload["important_lines"]) > 60:
return LONG_MODEL
return FAST_MODEL
def analyze_log(raw_log: str) -> dict[str, Any]:
compressed = compress_log(raw_log)
model = choose_model(compressed)
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(compressed, ensure_ascii=False),
},
],
temperature=0.1,
response_format={"type": "json_object"},
)
content = response.choices[0].message.content or "{}"
result = json.loads(content)
result["meta"] = {
"model": model,
"log_sha256": compressed["sha256"],
"original_lines": compressed["original_lines"],
"truncated": compressed["truncated"],
"usage": response.usage.model_dump() if response.usage else None,
}
return result
这段代码有几个刻意保留的工程细节:
- 请求模型前先替换 API Key、密码和手机号;
- 时间、请求 ID 和耗时数字会被归一化,便于统计重复错误;
- 只把警告和错误行作为主要证据,但保留原始行数与时间范围;
- 最终结果记录实际模型、日志哈希和 Token 用量,方便复盘;
- 超过长度限制的日志明确标记
truncated,不假装分析了全部内容。
3. FastAPI 接口
app.py:
python
from typing import Any
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from analyzer import analyze_log
app = FastAPI(title="Log Doctor", version="1.0.0")
class AnalyzeRequest(BaseModel):
log: str = Field(min_length=20, max_length=200000)
class AnalyzeResponse(BaseModel):
result: dict[str, Any]
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/analyze", response_model=AnalyzeResponse)
def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
try:
return AnalyzeResponse(result=analyze_log(request.log))
except ValueError as exc:
raise HTTPException(status_code=502, detail="模型返回了无效 JSON") from exc
启动服务:
powershell
uvicorn app:app --host 0.0.0.0 --port 8000
浏览器打开 http://127.0.0.1:8000/docs,可以看到 FastAPI 自动生成的接口页面。也可以直接使用 PowerShell 请求。
六、用一段故障日志验证
sample.log:
text
2026-08-18 21:03:11 ERROR gateway request_id=a1 upstream timed out, path=/api/orders, cost=30002ms
2026-08-18 21:03:12 ERROR order-api request_id=a1 sqlalchemy.exc.TimeoutError: QueuePool limit of size 10 overflow 20 reached
2026-08-18 21:03:14 WARN order-api request_id=b2 redis connection retry 1
2026-08-18 21:03:16 ERROR gateway request_id=c3 upstream timed out, path=/api/orders, cost=30001ms
2026-08-18 21:03:17 ERROR order-api request_id=c3 sqlalchemy.exc.TimeoutError: QueuePool limit of size 10 overflow 20 reached
2026-08-18 21:03:20 INFO user-api request_id=d4 GET /api/profile 200 cost=42ms
2026-08-18 21:03:26 ERROR gateway request_id=e5 upstream timed out, path=/api/orders, cost=30000ms
2026-08-18 21:03:27 ERROR order-api request_id=e5 sqlalchemy.exc.TimeoutError: QueuePool limit of size 10 overflow 20 reached
PowerShell 调用:
powershell
$body = @{
log = Get-Content .\sample.log -Raw
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8000/analyze `
-ContentType "application/json" `
-Body $body | ConvertTo-Json -Depth 10
这组日志返回的核心结果应接近下面的结构。模型措辞可能略有变化,判断必须以日志证据为准:
json
{
"result": {
"summary": "订单接口多次超时,order-api 同期出现数据库连接池耗尽,网关 502 很可能是下游请求阻塞造成的。",
"time_range": "2026-08-18 21:03:11 - 2026-08-18 21:03:27",
"services": ["gateway", "order-api", "user-api"],
"key_errors": [
{
"message": "order-api 数据库连接池耗尽",
"count": 3,
"evidence": "sqlalchemy.exc.TimeoutError: QueuePool limit of size 10 overflow 20 reached"
},
{
"message": "网关访问 /api/orders 超时",
"count": 3,
"evidence": "upstream timed out, path=/api/orders"
}
],
"root_causes": [
{
"cause": "order-api 无法及时取得数据库连接,导致请求堆积并触发网关超时",
"confidence": "high",
"evidence": "三次网关超时均与三次 QueuePool TimeoutError 同期出现"
},
{
"cause": "Redis 短暂重连放大了接口延迟",
"confidence": "low",
"evidence": "只出现一次 redis retry,证据不足"
}
],
"actions": [
"检查故障时段数据库活跃连接数、慢查询和连接占用时长",
"检查 order-api 连接是否在异常路径中未释放",
"核对 SQLAlchemy pool_size、max_overflow 与实例并发量",
"确认数据库恢复后再评估是否需要调整网关超时时间"
],
"missing_context": [
"数据库监控数据",
"order-api 实例数和请求并发量",
"完整异常堆栈"
],
"meta": {
"model": "/maas/deepseek-ai/DeepSeek-V3.2",
"original_lines": 8,
"truncated": false
}
}
}
这个结果里我最看重的不是"数据库连接池耗尽"这句话,而是它把高置信度判断和低置信度猜测分开了。Redis 只重连一次,不能仅凭这一行就认定它是主因。
截图 3:实际运行时可截取 FastAPI
/docs中/analyze的响应,画面保留summary、root_causes、actions和meta.model,日志中的业务信息需要脱敏。
七、一个实际会碰到的问题:模型返回 JSON,但程序仍然解析失败
只靠提示词约束输出时,很容易遇到一种失败:请求状态是 200,肉眼看返回内容也像 JSON,程序却报:
text
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
打印 repr(content) 后可以看到,模型可能在 JSON 前后加上 Markdown 代码围栏:
text
```json
{"summary": "..."}
```
这类输出给人看没问题,给 json.loads 就会失败。
只在提示词里写"只输出 JSON"并不稳妥。项目代码使用两层约束:
python
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0.1,
response_format={"type": "json_object"},
)
同时在 system prompt 中明确字段、枚举值和"不要输出 Markdown"。正式发布前,应准备至少 30 份脱敏日志跑结构化回归,并记录解析成功率;没有真实调用数据时,不应编造一个好看的通过率。
这里有个边界要说清:response_format 能提高格式稳定性,但不能保证字段内容一定正确。生产环境仍应使用 Pydantic 或 JSON Schema 校验字段,并把校验失败的结果送入人工复核,而不是直接写进事故记录。
八、另一个容易忽略的问题:日志脱敏不能只靠提示词
最初版本在提示词里写了"请忽略日志中的密码和 Token"。这并不等于安全,因为敏感内容已经随请求发送出去了。正确顺序应该是先在本地替换,再调用模型。
本文代码处理了四类常见内容:Bearer Token、API Key、password 字段和手机号。真实系统还要根据日志规范加入这些规则:
- 数据库连接串;
- Cookie、Session ID 和 JWT;
- 邮箱、身份证号、订单号;
- 内网 IP、主机名和租户 ID;
- 用户输入中可能包含的隐私文本。
如果无法确定日志是否允许发送到外部服务,就不要调用 MaaS。技术便利不能代替公司的数据合规要求。
九、如何部署
这个工具没有状态,适合直接用 Docker 运行。Dockerfile 可以这样写:
dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py analyzer.py ./
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
构建和启动:
powershell
docker build -t log-doctor:1.0 .
docker run --rm -p 8000:8000 --env-file .env log-doctor:1.0
部署到服务器后,还应在接口前加公司现有的鉴权,不要把 /analyze 裸露到公网。日志接口也不建议记录完整请求体,否则"分析日志的服务"又会复制一份敏感日志。
十、模型怎么分工更合理
同一项目使用多个模型时,我没有让它们互相讨论,而是按输入规模路由:
python
def choose_model(payload: dict) -> str:
if payload["original_lines"] > 300 or len(payload["important_lines"]) > 60:
return LONG_MODEL
return FAST_MODEL
短日志通常只有几十行,任务是分类和摘要,使用轻量模型足够。长日志可能包含多段堆栈和跨服务时间线,再切到长上下文模型。
上线前应该用自己的日志回归集比较:
| 检查项 | 判断方法 |
|---|---|
| 关键错误召回 | 人工标出的错误是否都进入 key_errors |
| 根因证据 | 每个根因是否能在日志中找到对应行 |
| 过度推断 | 是否出现日志没有提供的组件、版本和配置 |
| JSON 稳定性 | 能否通过固定 Schema 校验 |
| 成本 | 记录输入、输出 Token 和单次调用模型 |
| 延迟 | 记录 P50、P95,而不是只看最快的一次 |
模型切换后要重新跑同一批样本。接口兼容只说明代码可以调用,不代表不同模型的字段稳定性和判断习惯完全一样。
十一、这次实战得到的结论
这个日志助手没有替代值班工程师,也不应该自动执行扩容、重启或数据库操作。它真正省下的是事故刚发生时的整理时间:先把大量重复日志压成一张有证据的排障卡,再由人决定下一步。
蓝耘元生代在项目中的作用也很明确:提供统一的模型调用入口,让短日志和长日志可以在同一套 SDK 下选择不同模型。项目代码主要负责脱敏、压缩、校验和审计,模型负责理解上下文。
如果要把这个示例用于真实业务,我建议先做三件事:准备 20 到 50 份脱敏历史日志作为回归集;补全符合公司数据规范的脱敏规则;把输出限制在"建议",不让模型直接执行生产操作。
做完这三步,它才是一个排障工具,而不是把日志复制给聊天机器人。