For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 性能调优、监控接入、部署标准化、清理 Dify 遗留代码, 使 LangGraph sidecar 达到可长期维护的生产状态。
Architecture: Prometheus 指标暴露 + 结构化日志 + Docker Compose 生产编排 + Java 端废弃代码清理。
Tech Stack: Python 3.11, FastAPI, Prometheus (client), Docker Compose, SLF4J
/metrics 暴露 (Prometheus 拉取模式)D:\workspace\cfc\Files:
cfc-langgraph/app/tools/java_client.py (连接池复用)Modify: cfc-langgraph/app/rag/embeddings.py (缓存)
[x] Step 1: 优化 JavaClient 连接池
# app/tools/java_client.py — 确保 httpx.AsyncClient 复用连接池
import httpx
from typing import Optional
from app.config import settings
import logging
logger = logging.getLogger(__name__)
class JavaClient:
"""Java 后端 HTTP 客户端 (单例, 复用连接池)"""
_instance: Optional["JavaClient"] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._client = None
cls._instance._base_url = settings.java_base_url
return cls._instance
async def _get_client(self) -> httpx.AsyncClient:
if self._client is None:
limits = httpx.Limits(
max_connections=10,
max_keepalive_connections=5,
keepalive_expiry=30,
)
self._client = httpx.AsyncClient(
base_url=self._base_url,
timeout=httpx.Timeout(10.0, connect=3.0),
limits=limits,
)
return self._client
async def close(self):
if self._client:
await self._client.aclose()
self._client = None
JavaClient._instance = None
# ... 其余方法保持与 Phase 1/2/3 一致
[x] Step 2: Embedding 实例缓存 (单例模式)
# app/rag/embeddings.py — 已实现单例, 验证即可
# 确保 _embeddings 不会被重复创建
[x] Step 3: 添加请求耗时中间件
# 在 app/main.py 中追加
import time
from fastapi import Request
import logging
logger = logging.getLogger(__name__)
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
# 慢查询日志: > 5s 的记录
if elapsed > 5:
logger.warning("SLOW_REQUEST: %s %s took %.2fs",
request.method, request.url.path, elapsed)
else:
logger.debug("REQUEST: %s %s took %.2fs",
request.method, request.url.path, elapsed)
response.headers["X-Response-Time"] = f"{elapsed:.3f}s"
return response
[x] Step 4: ChromaDB 批量写入优化
知识库同步时使用批量写入替代逐条写入:
# app/tasks/knowledge_sync.py — 确认已使用 aadd_documents 批量 API
# 分块数 < 100 时一次提交, > 100 时分批
BATCH_SIZE = 100
async def sync_knowledge_base():
# ... 前置逻辑不变 ...
# 分批写入
for i in range(0, len(chunks), BATCH_SIZE):
batch = chunks[i:i + BATCH_SIZE]
await vectorstore.aadd_documents(batch)
vectorstore.persist()
logger.debug("知识库同步进度: %d/%d", i + len(batch), len(chunks))
# ... 后续逻辑不变 ...
[x] Step 5: Commit
git add cfc-langgraph/app/tools/java_client.py \
cfc-langgraph/app/main.py \
cfc-langgraph/app/tasks/knowledge_sync.py
git commit -m "perf(langgraph): connection pool, timing middleware, batch write"
Files:
cfc-langgraph/app/monitoring.pyModify: cfc-langgraph/app/main.py (注册 metrics 路由)
[x] Step 1: 创建 app/monitoring.py
"""Prometheus 监控指标"""
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from fastapi import APIRouter, Response
import time
import functools
# ── 指标定义 ──
# LLM 调用
llm_calls_total = Counter(
"llm_calls_total", "Total LLM API calls",
["model", "status"],
)
llm_duration_seconds = Histogram(
"llm_duration_seconds", "LLM call duration",
["model"],
buckets=(0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0),
)
llm_tokens_total = Counter(
"llm_tokens_total", "Total tokens used",
["model", "type"], # type = input / output
)
# RAG 检索
rag_retrievals_total = Counter(
"rag_retrievals_total", "Total RAG retrievals",
["method"], # method = vector / bm25 / ensemble
)
rag_duration_seconds = Histogram(
"rag_duration_seconds", "RAG retrieval duration",
["method"],
buckets=(0.01, 0.05, 0.1, 0.5, 1.0),
)
# Agent 调用
agent_calls_total = Counter(
"agent_calls_total", "Total Agent invocations",
["agent_type"], # agent_type = chat / recommend / analysis
)
agent_duration_seconds = Histogram(
"agent_duration_seconds", "Agent execution duration",
["agent_type"],
buckets=(0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0),
)
# Java 后端调用
java_calls_total = Counter(
"java_calls_total", "Total calls to Java backend",
["endpoint", "status"],
)
java_duration_seconds = Histogram(
"java_duration_seconds", "Java backend call duration",
["endpoint"],
buckets=(0.01, 0.05, 0.1, 0.5, 1.0, 2.0),
)
# 知识库同步
kb_sync_duration = Gauge(
"kb_sync_duration_seconds", "Last knowledge base sync duration"
)
kb_sync_documents = Gauge(
"kb_sync_documents_total", "Documents processed in last sync"
)
# ── 装饰器 ──
def monitor_agent(agent_type: str):
"""Agent 性能监控装饰器"""
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
agent_calls_total.labels(agent_type=agent_type).inc()
start = time.perf_counter()
try:
result = await func(*args, **kwargs)
agent_duration_seconds.labels(agent_type=agent_type).observe(
time.perf_counter() - start)
return result
except Exception as e:
agent_duration_seconds.labels(agent_type=agent_type).observe(
time.perf_counter() - start)
raise
return wrapper
return decorator
# ── Metrics 路由 ──
router = APIRouter(tags=["monitoring"])
@router.get("/metrics")
async def metrics():
return Response(
content=generate_latest(),
media_type="text/plain; charset=utf-8",
)
@router.get("/api/v1/health")
async def detailed_health():
"""详细健康检查 (含组件状态)"""
status = {"status": "ok", "components": {}}
# 检查 ChromaDB
try:
from app.config import settings
import os
chroma_path = settings.chroma_db_path
status["components"]["chromadb"] = {
"status": "ok",
"path": chroma_path,
"exists": os.path.exists(chroma_path),
}
except Exception as e:
status["components"]["chromadb"] = {"status": "error", "message": str(e)}
status["status"] = "degraded"
# 检查 LLM API
try:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
max_tokens=5,
)
await llm.ainvoke("ping")
status["components"]["llm"] = {"status": "ok"}
except Exception as e:
status["components"]["llm"] = {"status": "error", "message": str(e)}
status["status"] = "degraded"
# 检查 Java 后端
try:
from app.tools.java_client import JavaClient
client = JavaClient()
jc = await client._get_client()
resp = await jc.get("/health")
if resp.status_code == 200:
status["components"]["java_backend"] = {"status": "ok"}
else:
status["components"]["java_backend"] = {"status": "error", "code": resp.status_code}
status["status"] = "degraded"
except Exception as e:
status["components"]["java_backend"] = {"status": "error", "message": str(e)}
status["status"] = "degraded"
return status
[x] Step 2: 注册 metrics 路由到 app/main.py
from app import monitoring # 新增
app.include_router(monitoring.router) # 新增
[x] Step 3: 在 Agent 调用处插入监控装饰器
# app/graphs/recommend_graph.py — RecommendAgent.run()
from app.monitoring import monitor_agent
class RecommendAgent:
@monitor_agent("recommend")
async def run(self, query: str, tags: list[str], limit: int = 5) -> dict:
# ... 原有代码不变 ...
同理在 chat_graph.py 的 llm_call 节点和 analysis_graph.py 的 analyze 节点也加上。
[x] Step 4: 添加 prometheus-client 依赖到 pyproject.toml
dependencies = [
# ... 现有依赖保持不变 ...
"prometheus-client>=0.21",
]
[x] Step 5: 验证 metrics 端点
cd cfc-langgraph
pip install -e .
uvicorn app.main:app --port 9000 &
curl http://localhost:9000/metrics
# 预期: 以 # HELP / # TYPE 开头的 Prometheus 格式文本
kill %1
[x] Step 6: Commit
git add cfc-langgraph/app/monitoring.py \
cfc-langgraph/app/main.py \
cfc-langgraph/app/graphs/recommend_graph.py \
cfc-langgraph/app/graphs/chat_graph.py \
cfc-langgraph/app/graphs/analysis_graph.py \
cfc-langgraph/pyproject.toml
git commit -m "feat(langgraph): Prometheus metrics and detailed health check"
Files:
cfc-langgraph/app/log_config.pyModify: cfc-langgraph/app/main.py
[x] Step 1: 创建 app/log_config.py
"""结构化日志配置"""
import logging
import json
import sys
from datetime import datetime, timezone
class JsonFormatter(logging.Formatter):
"""JSON 日志格式化器 (适合生产环境日志聚合)"""
def format(self, record: logging.LogRecord) -> str:
log_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if hasattr(record, "trace_id"):
log_entry["trace_id"] = record.trace_id
if record.exc_info and record.exc_info[0]:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False)
def setup_logging(level: str = "INFO", json_format: bool = False):
"""配置日志
Args:
level: 日志级别
json_format: True=JSON 格式, False=开发友好格式
"""
root = logging.getLogger()
root.setLevel(getattr(logging, level.upper(), logging.INFO))
# 清除已有 handler
root.handlers.clear()
handler = logging.StreamHandler(sys.stdout)
if json_format:
handler.setFormatter(JsonFormatter())
else:
handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
))
root.addHandler(handler)
# 第三方库日志级别
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("chromadb").setLevel(logging.WARNING)
logging.getLogger("langchain").setLevel(logging.WARNING)
[x] Step 2: 在 app/main.py 启动时配置
@app.on_event("startup")
async def startup():
import os
json_logs = os.getenv("JSON_LOGS", "false").lower() == "true"
setup_logging(level=settings.log_level, json_format=json_logs)
# ... 其余初始化代码 ...
[x] Step 3: Commit
git add cfc-langgraph/app/log_config.py cfc-langgraph/app/main.py
git commit -m "feat(langgraph): structured JSON logging"
Files:
cfc-langgraph/docker-compose.yml (覆盖 Phase 1 的开发版)Create: cfc-langgraph/Dockerfile (多阶段构建)
[x] Step 1: 优化 Dockerfile (多阶段构建)
# ── 构建阶段 ──
FROM python:3.11-slim AS builder
WORKDIR /build
COPY pyproject.toml .
RUN pip install --no-cache-dir -e . && \
pip install --no-cache-dir gunicorn
# ── 运行阶段 ──
FROM python:3.11-slim
# 时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY app/ app/
COPY data/ data/ 2>/dev/null || true
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 9000
CMD ["gunicorn", "app.main:app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:9000", \
"--workers", "2", \
"--timeout", "60", \
"--keep-alive", "10", \
"--access-logfile", "-", \
"--error-logfile", "-"]
[x] Step 2: 创建生产 docker-compose.yml
version: "3.8"
services:
cfc-backend:
build:
context: ../cfc-backend
dockerfile: Dockerfile
ports:
- "9082:9082"
environment:
- SPRING_PROFILES_ACTIVE=prod
- python.enabled=true
- python.base-url=http://langgraph-svc:9000
networks:
- cfc-net
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9082/actuator/health"]
interval: 30s
timeout: 5s
retries: 3
langgraph-svc:
build:
context: .
dockerfile: Dockerfile
ports:
- "9000:9000"
env_file:
- .env.production
environment:
- JAVA_BASE_URL=http://cfc-backend:9082
- CHROMA_DB_PATH=/data/chroma_db
- JSON_LOGS=true
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}
volumes:
- langgraph-data:/data
networks:
- cfc-net
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
- cfc-backend
# Prometheus (可选: 指标采集)
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
networks:
- cfc-net
restart: unless-stopped
profiles:
- monitoring
networks:
cfc-net:
volumes:
langgraph-data:
prometheus-data:
[x] Step 3: 创建 Prometheus 配置
# cfc-langgraph/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "langgraph-svc"
static_configs:
- targets: ["langgraph-svc:9000"]
metrics_path: /metrics
[x] Step 4: 创建生产环境 .env.production 模板
# cfc-langgraph/.env.production (不上传 git, 手动部署时创建)
LLM_API_KEY=sk-your-production-key
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
EMBEDDING_API_KEY=${LLM_API_KEY}
EMBEDDING_BASE_URL=${LLM_BASE_URL}
EMBEDDING_MODEL=text-embedding-v3
LANGCHAIN_TRACING_V2=false
LANGCHAIN_API_KEY=
LANGCHAIN_PROJECT=cfc-langgraph-prod
LOG_LEVEL=info
JSON_LOGS=true
[x] Step 5: Commit
git add cfc-langgraph/Dockerfile \
cfc-langgraph/docker-compose.yml \
cfc-langgraph/prometheus.yml \
cfc-langgraph/.env.production
git commit -m "ops(langgraph): production Docker Compose with monitoring"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/DifySyncService.java (标记废弃)cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java (简化, 移除 Dify 直接调用)Delete: cfc-backend/src/main/resources/application.yml 中 dify.* 配置 (可选保留 Dify fallback)
[x] Step 1: 标记 DifySyncService 废弃
// DifySyncService.java 头部加 @Deprecated 注解
/**
* 知识库同步服务
* @deprecated 知识库同步已迁移到 LangGraph Python 服务 (cfc-langgraph/app/tasks/knowledge_sync.py)
* 计划在下一个大版本移除
*/
@Deprecated
@Service
public class DifySyncService {
// ... 代码保持不变 ...
}
[x] Step 2: 简化 AIService.java
Dify 相关的方法合并为单一的 fallback 块:
// AIService.java
// sendMessageToDify() 保留为私有 fallback 方法
// 公开的 sendMessage() 优先 Python, 仅当 Python 不可用时调用 sendMessageToDify()
/**
* 发送聊天消息 (自动选择后端)
* 优先 LangGraph Python 服务, 失败时回退 Dify
*/
public Map<String, Object> sendMessage(String query, String userId,
String conversationId,
Map<String, Object> inputs) {
// Phase 2 已实现的逻辑: aiGateway.chat() → fallback sendMessageToDify()
// 如果 aiGateway 长期稳定, 可在此处移除 Dify fallback
}
[x] Step 3: 清理 application.yml 中的 Dify 配置 (保留 fallback 选项)
# application.yml — 保留 dify.* 配置但标记为降级通道
dify:
base-url: http://dify.bianwoyou.cn/v1
api-key: ${DIFY_API_KEY:}
nutrition-api-key: ${DIFY_NUTRITION_API_KEY:}
tongue-api-key: ${DIFY_TONGUE_API_KEY:}
callback-secret: ${DIFY_CALLBACK_SECRET:}
# 注意: 以上配置仅作为 LangGraph 不可用时的降级通道
# 正常情况下应通过 python.enabled=true 启用 LangGraph
[x] Step 4: 编译验证
cd cfc-backend
mvn clean compile -q
# 预期: BUILD SUCCESS (可能有 @Deprecated 警告, 不影响编译)
[x] Step 5: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/DifySyncService.java \
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java \
cfc-backend/src/main/resources/application.yml
git commit -m "chore(backend): mark DifySyncService as deprecated, clean up AIService"
Files:
cfc-langgraph/docs/deployment.mdCreate: cfc-langgraph/docs/operations.md
[x] Step 1: 创建 docs/deployment.md
# LangGraph Sidecar 部署文档
## 前置依赖
- Docker + Docker Compose (推荐)
- 或 Python 3.11+ + pip (直接部署)
- LLM API Key (DeepSeek / OpenAI 兼容)
## 目录结构
cfc-langgraph/ ├── Dockerfile ├── docker-compose.yml ├── prometheus.yml ├── .env.production # 生产配置 (手动创建, 不上传 git) ├── app/ # 服务代码 │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置管理 │ ├── api/ # HTTP 接口 │ ├── agents/ # Agent 定义 │ ├── graphs/ # LangGraph StateGraph │ ├── tools/ # Agent Tool │ ├── rag/ # RAG Pipeline │ ├── memory/ # 三层记忆 │ └── tasks/ # 定时任务 ├── data/ │ └── chroma_db/ # ChromaDB 持久化 (自动创建) └── docs/
├── deployment.md # 本文件
└── operations.md # 运维手册
## Docker Compose 部署
bash
cp .env.example .env.production
docker-compose up -d
curl http://localhost:9000/api/v1/health
docker-compose logs -f langgraph-svc
## 直接部署 (无 Docker)
bash
pip install -e .
export LLM_API_KEY=sk-xxx export JAVA_BASE_URL=http://localhost:9082
gunicorn app.main:app \ --worker-class uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:9000 \ --workers 2 \ --timeout 60 \ --access-logfile - \ --error-logfile -
## 环境变量说明
| 变量 | 必填 | 说明 |
|------|------|------|
| LLM_API_KEY | 是 | LLM API Key |
| LLM_BASE_URL | 否 | 默认 https://api.deepseek.com/v1 |
| LLM_MODEL | 否 | 默认 deepseek-chat |
| JAVA_BASE_URL | 是 | Java 后端地址 |
| CHROMA_DB_PATH | 否 | ChromaDB 持久化路径, 默认 ./data/chroma_db |
| LOG_LEVEL | 否 | 日志级别, 默认 info |
| JSON_LOGS | 否 | 启用 JSON 日志格式, 默认 false |
## 健康检查
GET /api/v1/health
Response: { "status": "ok", "components": {
"chromadb": {"status": "ok", "path": "...", "exists": true},
"llm": {"status": "ok"},
"java_backend": {"status": "ok"}
} }
[x] Step 2: 创建 docs/operations.md
# LangGraph Sidecar 运维手册
## 日常监控
### 1. 健康检查
生产环境建议配置 30 秒定时健康检查:
bash
curl -f http://localhost:9000/api/v1/health
预期返回 `{"status":"ok"}`。组件降级时返回 `{"status":"degraded"}`。
### 2. Prometheus 指标
GET /metrics
关键指标:
kb_sync_duration_seconds # 知识库同步耗时
### 3. 日志
JSON 格式日志可直接接入 ELK / Loki:
json
{"timestamp":"2026-07-20T10:00:00","level":"WARN","logger":"app.main","message":"SLOW_REQUEST: POST /api/v1/chat took 8.23s"}
## 常见问题
### Python 服务无法启动
bash
pip list | grep -E "fastapi|langchain|langgraph"
python -c "from app.config import settings; print(settings.llm_model)"
netstat -ano | grep 9000
### LLM 调用失败
1. 检查 `.env.production` 中的 `LLM_API_KEY` 是否有效
2. 检查 `LLM_BASE_URL` 是否可访问
3. 查看日志: `docker-compose logs langgraph-svc | grep llm_call`
### ChromaDB 损坏
bash
rm -rf data/chroma_db/ docker-compose restart langgraph-svc
curl -X POST http://localhost:9000/api/v1/admin/kb-sync
### 知识库同步失败
bash
curl http://localhost:9082/health
curl -X POST http://localhost:9082/api/article/updated-since \ -H "Content-Type: application/json" \ -d '{"since":"2026-01-01T00:00:00","status":"published"}'
docker-compose exec langgraph-svc python -c " import asyncio from app.tasks.knowledge_sync import sync_knowledge_base asyncio.run(sync_knowledge_base()) "
### Java 端回退 Dify
如果 Python 服务异常, Java 会自动回退 Dify:
yaml
python: enabled: true circuit-breaker:
failure-threshold: 3
reset-timeout-ms: 30000
熔断器打开时, Java 侧日志会输出 `AiGateway 熔断器已打开`, 等待 `reset-timeout-ms` 后自动半开重试。
## 扩缩容
LangGraph 服务是无状态的 (ChromaDB 在共享存储上):
yaml
services: langgraph-svc:
deploy:
replicas: 2
注意: ChromaDB 文件模式不支持并发写入, 多副本时知识库同步需加锁或切 PGVector。
## 备份
bash
tar czf chromabackup$(date +%Y%m%d).tar.gz data/chroma_db/
[x] Step 3: Commit
git add cfc-langgraph/docs/
git commit -m "docs(langgraph): deployment and operations manual"