Appearance
日志助手项目结构
ops-log-agent 是一个放在本地运行的命令行项目。输入是样例日志和 Runbook,输出是日志证据、检索记录和 Markdown 报告。现在没有 Web 页面、数据库、队列和日志平台接入,所有材料都放在项目目录里,方便看清楚每个文件到底干什么。
前面几块知识到这里开始合在一起。函数调用负责让模型向 Python 要资料,RAG 负责从资料里找相关片段,Agent 循环负责多轮工具调用,Skill 负责固定分析时的写法和边界。换到日志场景里,这几个东西分别落成日志读取函数、Runbook 检索函数、入口脚本和 log-triage 规则。
一、目录结构
项目根目录叫 ops-log-agent。当前目录先放这些文件:
text
ops-log-agent/
config.py
pyproject.toml
inputs/
sample.log
outputs/
runbooks/
common.md
scripts/
__init__.py
read_logs.py
build_index.py
run_agent.py
skills/
log-triage/
SKILL.mdinputs/ 放原始材料,runbooks/ 放排查资料,skills/ 放固定分析规则。outputs/ 是运行结果目录,证据 JSON、Runbook 索引、报告和 trace 都写到这里。这样输入和输出不混在一起,改样例日志时也不容易把生成文件当成原始材料。
新增空文件:scripts/__init__.py
这个文件不用写内容,它只负责让 scripts 目录可以按模块运行。后面的命令都从项目根目录执行:
bash
uv run python -m scripts.run_agent这样 scripts/read_logs.py、scripts/build_index.py 里导入根目录的 config.py 会更稳。如果直接写成 uv run python scripts/read_logs.py,Python 的模块搜索路径会落到 scripts/ 目录,根目录里的 config.py 可能找不到。这个坑很小,但项目刚搭起来时特别容易卡住。
二、依赖文件
pyproject.toml 放在项目根目录。当前项目只需要 OpenAI SDK 和 Pydantic;日志解析、Runbook 切分、JSON 读写都用标准库。
新增文件:pyproject.toml
toml
[project]
name = "ops-log-agent"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"openai>=1.0.0",
"pydantic>=2.0.0",
]openai 用在模型调用和工具调用。pydantic 留给结构化报告校验,当前日志证据用 dataclass 写,代码会更轻一点。这里不接数据库和向量库,Runbook 检索用本地 JSON 索引看清楚形状。
三、配置文件
多个脚本都会读同一份日志、同一个 Runbook 目录、同一个输出目录。路径如果散在每个脚本里,改一次目录就要到处找。根目录下的 config.py 只放这些共享路径和少量运行参数。
新增文件:config.py
python
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
INPUT_LOG = BASE_DIR / "inputs" / "sample.log"
RUNBOOK_DIR = BASE_DIR / "runbooks"
OUTPUT_DIR = BASE_DIR / "outputs"
# 一次最多取几段 Runbook 放进分析材料。
MAX_TOP_K = 3
# Agent 最多允许几轮工具调用,避免模型反复查同一个工具。
MAX_STEPS = 4路径都从 config.py 所在目录算起。命令在哪个目录里被调用,脚本拿到的 INPUT_LOG 都指向同一个文件。这个细节对命令行项目很重要,否则在 IDE、终端、定时任务里运行,工作目录一变,脚本就开始找不到文件。
四、样例日志
项目现在只有一份小日志,放在 inputs/sample.log。它不是为了模拟完整生产日志,只是给解析器一个固定输入。
新增文件:inputs/sample.log
text
2026-06-18 09:20:01 INFO service=api request_id=req-1001 path=/login status=200 cost_ms=42
2026-06-18 09:20:02 WARN service=api request_id=req-1002 path=/orders status=502 cost_ms=2100
2026-06-18 09:20:03 ERROR service=worker task=sync_orders order_id=8802 msg="upstream timeout"
2026-06-18 09:20:04 INFO service=api request_id=req-1003 path=/orders status=200 cost_ms=58这四行里有两条明显线索:/orders 返回 502,耗时 2100ms;后台任务 sync_orders 里有 upstream timeout。解析脚本会把这两条整理成证据,保留它们前后相邻的日志行。日志字段故意写得直一点,时间、级别、service、path、status、cost_ms 都能被正则拆出来。
真实日志可能是 JSON,也可能来自 Loki、Elasticsearch、文件或对象存储。当前项目只处理这一种文本格式,格式变化时先改 scripts/read_logs.py,不要让模型直接猜原始日志。
五、Runbook 文件
Runbook 放排查线索,不放故障结论。日志里出现 /orders、502、upstream timeout 时,检索函数应该能命中“订单同步失败”这一段。
新增文件:runbooks/common.md
markdown
# 常见排查
## 登录接口超时
- 查看 `/login` 是否集中出现 5xx
- 查看接口耗时是否超过 1000ms
- 查看后端和认证服务日志
## 订单同步失败
- 查看 `/orders` 是否出现 502 或 503
- 查看 `task=sync_orders` 是否有错误
- 看到 `upstream timeout` 时,检查上游服务日志和 worker 重试记录这里的 Runbook 更像排查便签。它告诉脚本和模型“看到什么线索时该联想到哪类问题”,但不会替模型写故障报告。报告仍然要根据日志证据生成。
六、Skill 文件
skills/log-triage/SKILL.md 记录分析日志时的输出格式和证据边界。它不读文件,也不查 Runbook,只管分析时哪些内容必须保留、哪些内容不能乱补。
新增文件:skills/log-triage/SKILL.md
markdown
---
name: log-triage
description: 分析日志告警、整理排查证据、引用 Runbook 并输出简短结论时使用。
---
# 日志排查
输出分成四块:
- 现象
- 证据
- Runbook 命中
- 结论
证据里保留 request_id、service、path、status、cost_ms 和错误消息。
资料不足时写“证据不足”,不要补不存在的服务名、机器名或负责人。这个文件对应 Skills 那一篇里的 SKILL.md。Runbook 是资料,Skill 是处理资料的方法。把这两个分开以后,Runbook 可以继续补排查条目,Skill 只管报告该怎么写、证据该怎么引用。
七、入口文件
scripts/run_agent.py 当前还是入口壳,只确认配置路径能被读到。真正的日志解析会放到 scripts/read_logs.py,Runbook 索引会放到 scripts/build_index.py,入口脚本负责把它们接起来。
新增文件:scripts/run_agent.py
python
#!/usr/bin/env python3
"""日志助手入口。"""
from config import INPUT_LOG, OUTPUT_DIR, RUNBOOK_DIR
def main() -> int:
print("ops-log-agent")
print(f"log: {INPUT_LOG}")
print(f"runbooks: {RUNBOOK_DIR}")
print(f"outputs: {OUTPUT_DIR}")
return 0
if __name__ == "__main__":
raise SystemExit(main())从 ops-log-agent/ 根目录运行:
bash
uv run python -m scripts.run_agent能看到类似输出:
text
ops-log-agent
log: .../ops-log-agent/inputs/sample.log
runbooks: .../ops-log-agent/runbooks
outputs: .../ops-log-agent/outputs这一步只验证项目骨架和导入路径。目录、配置、样例日志、Runbook、Skill 和入口文件都有位置以后,日志解析器再接到 scripts/read_logs.py 里。