feat: 添加结构化日志系统,更新LLM配置与全部文档

新增:
- backend/logger.py — 集中日志模块 (JSON格式 + trace_id + 独立llm.log)
- @log_node / @_log_route 装饰器覆盖17个节点和8个路由

改进:
- backend/llm.py — _LLMLoggingWrapper 自动记录LLM输入输出
- backend/llm.py — API Key优先读ANTHROPIC_API_KEY,模型名改为MiniMax-M2.7
- backend/llm.py — get_llm() 新增caller参数标识调用来源
- backend/validation.py — 新增验证结果/连接失败日志
- backend/session.py — 新增会话创建/删除日志
- app.py — 新增用户交互日志 (输入/执行/异常/会话操作)
- app.py — 提前导入torchvision抑制transformers懒加载报错
- .env.example — 新增LOG_DIR/LOG_LEVEL/ANTHROPIC_API_KEY等配置项
- .gitignore — 新增logs/和db/忽略规则

文档:
- ROADMAP.md — 新增阶段四: 可观测性
- README.md — 补充日志架构/LLM配置/项目结构
- CLAUDE.md — 同步最新配置/日志/MAX_RETRY(3)
- CODE_GUIDE.md — 新增第15章日志系统,更新架构图/LLM/配置
This commit is contained in:
2026-05-19 23:40:01 +08:00
parent 6467fd4ae5
commit 067880bf2e
13 changed files with 753 additions and 82 deletions
+145 -42
View File
@@ -29,7 +29,7 @@
## 1. 项目是什么
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML 模板 → 自动验证 → 失败则自动修正(最多 5 次)→ 重试耗尽后失败上下文自动注入下一轮 → 返回可用的 JRXML 文件。
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML 模板 → 自动验证 → 失败则自动修正(最多 3 次)→ 重试耗尽后失败上下文自动注入下一轮 → 返回可用的 JRXML 文件。
**技术栈**StreamlitUI + LangGraph(状态机) + LLMMiniMax/OpenAI/Ollama + ChromaDB(向量库) + FastAPI(验证微服务)
@@ -114,9 +114,10 @@ streamlit run app.py --server.port 8501
┌──────────┐ ┌──────────────┐ ┌───────────────┐
│backend/ │ │prompts/ │ │validation_ │
│llm.py │ │loader.py │ │service/main.py│
rag_ │ │*.md (7个 │ │(FastAPI, │
adapter.py│ │Prompt模板) │ │独立进程) │
error_kb │ └──────────────┘ └───────────────┘
logger.py │ │*.md (7个 │ │(FastAPI, │
rag_ │ │Prompt模板) │ │独立进程) │
adapter.py│ └──────────────┘ └───────────────┘
│error_kb │
│.py │
│embeddings│
│.py │
@@ -133,7 +134,7 @@ streamlit run app.py --server.port 8501
### 关键设计决策
1. **LLM 调用不经过 LangChain**`backend/llm.py` 直接使用 Anthropic SDK 和 OpenAI SDK,仅 Ollama 保留 langchain-ollama 包装。原因是 Anthropic SDK 需要透传 `api_key` 到 MiniMax 兼容端点
1. **LLM 调用不经过 LangChain**`backend/llm.py` 直接使用 Anthropic SDK 和 OpenAI SDK,仅 Ollama 保留 langchain-ollama 包装。所有 LLM 调用通过 `_LLMLoggingWrapper` 自动记录输入输出到 `logs/llm.log`
2. **Prompt 热重载**`prompts/loader.py` 每次都从磁盘读取 `.md` 文件,修改 Prompt 无需重启。
@@ -141,6 +142,8 @@ streamlit run app.py --server.port 8501
4. **验证服务独立进程**FastAPI 运行在 8001 端口,主进程通过 HTTP 调用。这样可以把 Java 编译验证加进去而不影响 UI 进程。
5. **结构化日志**`backend/logger.py` 提供 JSON 格式化日志,`trace_id` 通过 contextvars 贯穿全链路,LLM 调用与业务日志分离。
---
## 4. 数据总线:AgentState
@@ -217,7 +220,7 @@ def route_after_correct(state) -> Literal["validate", "finalize"]:
return "validate" if state.get("retry_count", 0) < MAX_RETRY else "finalize"
```
**MAX_RETRY 默认为 5**`.env` 中配置)。重试耗尽后进入 finalize,finalize 会将失败上下文写入 `pending_failure_context`,下次用户输入时 `process_input` 自动注入。
**MAX_RETRY 默认为 3**`.env` 中配置)。重试耗尽后进入 finalize,finalize 会将失败上下文写入 `pending_failure_context`,下次用户输入时 `process_input` 自动注入。
```
**关键路由逻辑**
@@ -503,12 +506,21 @@ class _BaseLLM:
raise NotImplementedError
```
### 7.1 三个实现
### 7.1 日志包装器
所有 LLM 实例都通过 `_LLMLoggingWrapper` 包装,自动记录:
- 请求 prompt(完整内容,截断 10000 字符)
- 响应内容(完整内容,截断 10000 字符)
- 调用耗时(毫秒)
- 模型名称、后端、调用来源(caller 参数)
日志输出到 `logs/llm.log`(独立于业务日志)。
### 7.2 三个实现
**MiniMaxLLM**`LLM_PROVIDER=anthropic`):
- 使用原始 `anthropic` SDK`from anthropic import Anthropic`
- `api_key` 必须显式传入构造函数(SDK 不会自动读 `OPENAI_API_KEY`
- `NO_PROXY=*` 绕过 Windows 代理
- API Key 优先读 `ANTHROPIC_API_KEY`fallback `OPENAI_API_KEY`
- `invoke()` 遍历 `resp.content``type == "text"` 的 block
- `stream()` 使用 `client.messages.stream()` + `s.text_stream`
@@ -520,11 +532,11 @@ class _BaseLLM:
- 使用 langchain-ollama 的 `ChatOllama`
- 本地运行,无需网络
### 7.2 调用约定
### 7.3 调用约定
所有节点统一使用:
所有节点统一使用,传入 `caller` 参数标识调用来源
```python
llm = get_llm()
llm = get_llm(caller="classify_intent")
# 同步
resp = llm.invoke(prompt)
text = resp.content.strip()
@@ -815,11 +827,90 @@ generate_session_id() → str # UUID hex[:12]
---
## 15. Streamlit UIapp.py
## 15. 日志系统:logger.py
`app.py` 是整个系统的入口,约 500 行。分为几个区域:
`backend/logger.py` 提供结构化日志能力,是整个系统的"黑匣子"。
### 15.1 组件树
### 15.1 架构设计
```
backend/logger.py
├── JsonFormatter JSON 单行格式化,自动收集 extra 字段
├── get_logger(name) 获取 loggername="llm" → llm.log,其他 → app.log
├── generate_trace_id() 生成 16 位 hex trace_id
├── set_trace_id(tid) 通过 contextvars 设置当前请求的 trace_id
└── get_trace_id() 获取当前 trace_id(自动跨线程/协程传播)
```
### 15.2 日志文件
| 文件 | 对应 logger | 内容 |
|------|-----------|------|
| `logs/app.log` | `get_logger("agent")`, `get_logger("app")`, `get_logger("session")`, `get_logger("validation")` | 节点流转、路由决策、用户交互、会话操作、验证结果 |
| `logs/llm.log` | `get_logger("llm")` | LLM 请求 prompt、响应内容、耗时、异常 |
### 15.3 日志格式
每条日志是单行 JSON
```json
{
"timestamp": "2026-05-19T23:05:22.877+08:00",
"level": "INFO",
"logger": "jrxml.agent",
"trace_id": "b29010ab4a014249",
"message": "[节点入口] classify_intent",
"module": "nodes",
"function": "wrapper",
"line": 53,
"extra": {
"node": "classify_intent",
"phase": "entry",
"state": {
"session_id": "681e55231bab",
"intent": "",
"has_jrxml": false,
"retry_count": 0
}
}
}
```
### 15.4 trace_id 机制
每次在 [app.py](file:///d:/Idea%20Project/jaspersoft/app.py) 的 `run_agent()` 中调用 `set_trace_id(generate_trace_id())`,后续所有节点、路由、LLM 调用都自动带上同一个 trace_id。通过 `grep "b29010ab4a014249" logs/*.log` 可还原一次请求的完整链路。
### 15.5 `@log_node` 装饰器
[agent/nodes.py](file:///d:/Idea%20Project/jaspersoft/agent/nodes.py) 中 17 个节点均使用 `@log_node("节点名")` 装饰器,自动记录:
- **入口日志** — 节点开始执行时的 state 摘要
- **出口日志** — 节点完成时的 state 摘要 + 耗时 (duration_ms)
- **异常日志** — 节点抛异常时的错误信息 + state 摘要
### 15.6 `@_log_route` 装饰器
[agent/graph.py](file:///d:/Idea%20Project/jaspersoft/agent/graph.py) 中 8 个路由函数均使用 `@_log_route("路由名")`,自动记录每次路由决策(from → to)。
### 15.7 日志分析示例
```bash
# 按 trace_id 追踪一次完整请求
jq 'select(.trace_id=="b29010ab4a014249")' logs/app.log
# 统计各节点平均耗时
jq 'select(.extra.phase=="exit") | {node: .extra.node, ms: .extra.duration_ms}' logs/app.log | jq -s 'group_by(.node) | map({node: .[0].node, avg_ms: (map(.ms) | add / length)})'
# 查看所有 LLM 调用耗时
jq 'select(.extra.direction=="response") | {caller: .extra.caller, ms: .extra.duration_ms}' logs/llm.log
```
---
## 16. Streamlit UIapp.py
`app.py` 是整个系统的入口,约 560 行。分为几个区域:
### 16.1 组件树
```
st.set_page_config (wide layout)
@@ -842,7 +933,7 @@ st.set_page_config (wide layout)
└── 触发 run_agent(full_prompt)
```
### 15.2 run_agent() — 核心渲染函数
### 16.2 run_agent() — 核心渲染函数
```python
def run_agent(user_input):
@@ -859,7 +950,7 @@ def run_agent(user_input):
# 6. 渲染总结卡片:用 agent_state(完整状态)而非 node_state(仅含变更字段)
```
### 15.3 文件上传流程
### 16.3 文件上传流程
```
用户选择文件
@@ -883,7 +974,7 @@ template_type?
清空 uploaded_files
```
### 15.4 流式渲染细节
### 16.4 流式渲染细节
```python
# 在 graph.stream 循环中:
@@ -896,7 +987,7 @@ elif mode == "custom":
这里 `streaming_placeholder.code()` 每次都会被覆盖,显示累积到当前的所有文本,产生"逐字打字"的视觉效果。
### 15.5 Ctrl+C 修复
### 16.5 Ctrl+C 修复
Streamlit 默认会在非输入元素上按 `c` 键清除缓存。注入 JS 拦截裸 `c` 键(不含 Ctrl/Alt/Meta):
@@ -912,7 +1003,7 @@ parent.addEventListener('keydown', function(e) {
---
## 16. 配置参考
## 17. 配置参考
所有配置通过 `.env` 文件管理。完整配置项:
@@ -920,10 +1011,12 @@ parent.addEventListener('keydown', function(e) {
|------|--------|------|
| `LLM_BACKEND` | `cloud` | `cloud``local` |
| `LLM_PROVIDER` | `openai` | `openai``anthropic` |
| `LLM_MODEL` | `gpt-4o` | 云端模型名 |
| `LLM_MODEL` | `MiniMax-M2.7` | 云端模型名 |
| `LOCAL_LLM_MODEL` | `qwen2.5-coder:7b` | Ollama 模型名 |
| `OPENAI_API_KEY` | — | API 密钥(Anthropic 模式也用这个 |
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | API 端点 |
| `OPENAI_API_KEY` | — | API 密钥(Anthropic 模式的 fallback |
| `ANTHROPIC_API_KEY` | — | Anthropic 兼容 API 密钥(优先) |
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI API 端点 |
| `ANTHROPIC_BASE_URL` | `https://api.minimaxi.com/anthropic` | Anthropic 兼容端点 |
| `EMBED_BACKEND` | `local` | `local``cloud` |
| `RAG_EMBED_MODEL` | `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 嵌入模型 |
| `RAG_CHROMA_PATH` | `./db/chroma` | ChromaDB 存储路径 |
@@ -931,17 +1024,19 @@ parent.addEventListener('keydown', function(e) {
| `RAG_USE_GPU` | `true` | GPU 加速 |
| `RAG_USE_FP16` | `true` | 半精度推理 |
| `VALIDATION_SERVICE_URL` | `http://localhost:8001/validate` | 验证服务地址 |
| `MAX_RETRY` | `5` | 最大自动修正次数 |
| `MAX_RETRY` | `3` | 最大自动修正次数 |
| `CONTEXT_MAX_TOKENS` | `6000` | 触发压缩的 token 阈值 |
| `CONTEXT_KEEP_RECENT` | `4` | 压缩时保留最近 N 轮 |
| `SESSIONS_DIR` | `./sessions` | 会话文件目录 |
| `HISTORY_MAX_SNAPSHOTS` | `10` | 撤销快照栈深度 |
| `LOG_DIR` | `./logs` | 日志目录 |
| `LOG_LEVEL` | `DEBUG` | 日志级别 (DEBUG/INFO/WARNING/ERROR) |
---
## 17. 如何添加新功能
## 18. 如何添加新功能
### 17.1 添加新的意图类型
### 18.1 添加新的意图类型
假设要添加"导出 Excel"功能:
@@ -951,7 +1046,7 @@ parent.addEventListener('keydown', function(e) {
4. **`app.py`** — 侧边栏添加快捷按钮
5. 如果 Excel 导出的逻辑复杂,可以新增节点 `handle_export_excel`
### 17.2 添加新的 LLM 后端
### 18.2 添加新的 LLM 后端
`backend/llm.py``get_llm()` 中添加新的 provider
@@ -965,7 +1060,7 @@ elif provider == "my_provider":
return MyProviderLLM()
```
### 17.3 添加新的文件格式支持
### 18.3 添加新的文件格式支持
`backend/file_parser.py` 中:
@@ -973,19 +1068,19 @@ elif provider == "my_provider":
2. 实现对应的 `_parse_xxx()` 函数
3.`app.py``file_uploader``type` 参数中加入新后缀
### 17.4 添加新的验证规则
### 18.4 添加新的验证规则
`validation_service/main.py``_check_structural_issues()` 中添加检查逻辑,返回描述问题的人类可读字符串即可。
### 17.5 修改 Prompt
### 18.5 修改 Prompt
直接编辑 `prompts/*.md`,保存后立即生效。Prompt 使用 Python `str.format()` 占位符,变量名必须与节点中 `.format()` 的参数名一致。
---
## 18. 调试指南
## 19. 调试指南
### 18.1 常见问题
### 19.1 常见问题
**Q: 验证服务连接失败**
```
@@ -1013,20 +1108,28 @@ elif provider == "my_provider":
**Q: 修改了 nodes.py 但不生效**
→ Streamlit 有热重载,保存文件后刷新浏览器即可。如果改的是 Prompt 文件,下次请求自动生效,无需做任何操作。
### 18.2 日志调试
### 19.2 日志调试
当前项目没有集中的日志系统。最简单的方式是在节点中加 `print()`
项目已集成结构化日志系统(详见第 15 章)。调试时
```python
# 在 nodes.py 中
def generate(state):
print(f"[DEBUG] Prompt length: {len(prompt)}")
print(f"[DEBUG] Generated JRXML length: {len(jrxml)}")
```bash
# 实时查看日志
tail -f logs/app.log | jq .
tail -f logs/llm.log | jq .
# 按 trace_id 追踪一次完整请求
jq 'select(.trace_id=="abc123")' logs/app.log
# 查看最近 5 次 LLM 调用
tail -5 logs/llm.log | jq '{caller: .extra.caller, model: .extra.model, ms: .extra.duration_ms}'
# 查看错误日志
jq 'select(.level=="ERROR")' logs/app.log
```
`print()` 输出会出现在 Streamlit 终端(终端 2
也可直接在 Streamlit 终端(终端 2添加 `print()` 快速调试
### 18.3 状态检查
### 19.3 状态检查
`app.py``run_agent()` 完成后,`st.session_state.agent_state` 包含最新状态。可以通过 Streamlit 的 `st.write()` 临时打印:
@@ -1035,7 +1138,7 @@ def generate(state):
st.json(state) # 打印完整状态(调试用,记得删除)
```
### 18.4 关键数据点
### 19.4 关键数据点
调试时最常需要检查的数据: