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:
+145
-42
@@ -29,7 +29,7 @@
|
||||
|
||||
## 1. 项目是什么
|
||||
|
||||
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML 模板 → 自动验证 → 失败则自动修正(最多 5 次)→ 重试耗尽后失败上下文自动注入下一轮 → 返回可用的 JRXML 文件。
|
||||
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML 模板 → 自动验证 → 失败则自动修正(最多 3 次)→ 重试耗尽后失败上下文自动注入下一轮 → 返回可用的 JRXML 文件。
|
||||
|
||||
**技术栈**:Streamlit(UI) + LangGraph(状态机) + LLM(MiniMax/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 UI:app.py
|
||||
## 15. 日志系统:logger.py
|
||||
|
||||
`app.py` 是整个系统的入口,约 500 行。分为几个区域:
|
||||
`backend/logger.py` 提供结构化日志能力,是整个系统的"黑匣子"。
|
||||
|
||||
### 15.1 组件树
|
||||
### 15.1 架构设计
|
||||
|
||||
```
|
||||
backend/logger.py
|
||||
├── JsonFormatter JSON 单行格式化,自动收集 extra 字段
|
||||
├── get_logger(name) 获取 logger(name="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 UI:app.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 关键数据点
|
||||
|
||||
调试时最常需要检查的数据:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user