25 KiB
技术债务分析报告
本文档总结了 TravelContentCreator 项目中的主要技术债务,为后续重构提供参考。
1. 数据库双端访问问题
现状
Python 端和 Java 端同时直接访问同一个数据库,各自维护独立的连接池和查询逻辑。
┌─────────────────┐ ┌─────────────────┐
│ Java 后端 │ │ Python AIGC │
│ (zwy_picture) │ │ (TravelContent) │
└────────┬────────┘ └────────┬────────┘
│ │
│ 独立连接池 │ 独立连接池
│ 独立 ORM │ 独立 SQL
▼ ▼
┌─────────────────────────────────┐
│ MySQL (travel_content) │
└─────────────────────────────────┘
问题
| 问题 | 影响 |
|---|---|
| 数据一致性风险 | 两端可能读到不同状态的数据 |
| 连接池资源浪费 | 两套连接池,占用更多连接数 |
| Schema 同步困难 | 表结构变更需要同时修改两端代码 |
| 事务边界模糊 | 跨服务操作无法保证事务一致性 |
| 代码重复 | 相同的查询逻辑在两端各写一遍 |
涉及文件
Python 端:
api/services/database_service.py- 独立的数据库服务api/services/prompt_service.py- 也有自己的连接池infrastructure/database/connection.py- 新架构的连接管理
Java 端:
ProductPackageService- 套餐查询MaterialService- 素材查询- 各种 Mapper/Repository
建议方案
方案 A: Python 作为纯算法服务
Java 后端 ──HTTP──> Python AIGC
│ │
│ │ (无数据库访问)
▼ │
MySQL <──────────────┘
- Python 只接收 Java 传来的完整数据
- 所有数据库操作由 Java 统一管理
- Python 变成无状态的计算服务
方案 B: 数据库访问层抽象
- 创建统一的数据访问 API (REST/gRPC)
- 两端通过 API 访问数据,不直接连数据库
2. 图片 Base64 传输问题
现状
Java 端从 S3 下载图片 → 转 Base64 → HTTP 传给 Python → Python 解码 → 处理
# poster.py L266
image_bytes = base64.b64decode(first_image_base64)
// PosterGenerateServiceImpl.java L327
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
问题
| 问题 | 影响 |
|---|---|
| 带宽浪费 | Base64 编码增加 33% 数据量 |
| 内存压力 | 大图片需要完整加载到内存 |
| 延迟增加 | 编解码耗时 + 传输耗时 |
| HTTP 请求体过大 | 多图场景可能超过限制 |
| 重复传输 | 同一图片可能被多次传输 |
数据示例
原始图片: 2MB
Base64 后: 2.67MB (+33%)
HTTP 传输: 2.67MB
Python 解码: 2MB 内存占用
建议方案
方案 A: URL 引用模式
Java 端: 上传图片到 S3,返回 URL
Python 端: 直接从 S3/CDN 下载
方案 B: 共享存储
Java 端: 保存图片到共享目录 /data/images/{id}.png
Python 端: 直接读取文件路径
方案 C: 图片 ID 引用
{
"image_ids": [123, 456],
"image_source": "s3"
}
Python 端根据 ID 自行获取图片
3. ppid / sid / pid 混乱问题
现状
系统中存在多种 ID 体系,命名不一致,转换逻辑分散:
| 缩写 | 全称 | 说明 |
|---|---|---|
ppid |
Product Package ID | 套餐 ID (Java 端主用) |
pid |
Product ID | 产品 ID |
sid |
Scenic Spot ID | 景区 ID |
scenic_spot_id |
- | Python 端用的景区 ID |
product_id |
- | Python 端用的产品 ID |
问题
# topic_generate.py - 需要手动解析 ppid
if ppid and (not scenic_spot_id or not product_id):
resolved = await self.db.resolve_ppid(ppid)
scenic_spot_id = resolved.get('scenic_spot_id')
product_id = resolved.get('product_id')
// AigcCompatibilityServiceImpl.java - Java 端也要解析
OriginalIds originalIds = getOriginalIdsByPackageId(packageId);
| 问题 | 影响 |
|---|---|
| 命名不一致 | ppid vs packageId vs product_package_id |
| 解析逻辑重复 | Java 和 Python 各自实现一遍 |
| 参数传递混乱 | 有时传 ppid,有时传 sid+pid |
| 向后兼容负担 | 需要同时支持多种参数格式 |
涉及文件
domain/aigc/engines/*.py- 每个引擎都要处理 ppidinfrastructure/database/repositories/product_package_repository.pyAigcCompatibilityServiceImpl.java
建议方案
统一入口点:
前端/调用方 ──ppid──> Java 后端 ──解析──> sid + pid ──> Python
- 所有 ppid 解析在 Java 端完成
- Python 端只接收已解析的
scenic_spot_id和product_id - 消除 Python 端的 ppid 解析逻辑
4. Prompt 拼接方式问题
现状
Prompt 构建分散在多个地方,使用字符串拼接和模板文件混合:
# prompt_builder.py L84-90
user_prompt = template.build_user_prompt(
style_content=style_content,
demand_content=demand_content,
object_content=object_content,
product_content=product_content,
refer_content=refer_content
)
# prompts.py L64-72
def build_user_prompt(self, **kwargs) -> str:
return self.user_template.format(**kwargs)
问题
| 问题 | 影响 |
|---|---|
| 模板文件分散 | 模板在 prompts/ 目录,配置在 config/ |
| 变量命名不统一 | style_content vs style vs styleName |
| 难以调试 | 最终 prompt 难以追踪 |
| 版本管理困难 | prompt 变更难以追踪和回滚 |
| 缺乏验证 | 缺少 prompt 有效性检查 |
涉及文件
utils/prompts.py- 基础模板类api/services/prompt_builder.py- 内容 prompt 构建api/services/prompt_service.py- prompt 服务prompts/*.txt- 模板文件
建议方案
方案 A: 集中式 Prompt 管理
class PromptRegistry:
def get_prompt(self, name: str, version: str = "latest") -> PromptTemplate:
"""从数据库或配置中心获取 prompt"""
pass
def render(self, name: str, context: dict) -> str:
"""渲染 prompt"""
pass
方案 B: Prompt 配置化
# prompts.yaml
topic_generate:
version: "1.2.0"
system: |
你是一个旅游营销专家...
user: |
请为 {scenic_spot} 生成 {num_topics} 个选题...
variables:
- scenic_spot: required
- num_topics: default=5
5. 工具使用方式问题
现状
各种工具类和服务的使用方式不统一:
# 方式1: 全局单例
from api.dependencies import get_ai_agent, get_config
# 方式2: 构造函数注入
class TopicGenerator:
def __init__(self, ai_agent: AIAgent, config_manager: ConfigManager):
# 方式3: 延迟加载
def _get_tweet_service(self):
if self._tweet_service:
return self._tweet_service
from api.services.tweet import TweetService
self._tweet_service = TweetService(...)
# 方式4: 直接实例化
db_service = DatabaseService(config_manager)
问题
| 问题 | 影响 |
|---|---|
| 依赖注入不一致 | 有的用单例,有的用构造函数 |
| 循环导入风险 | 延迟导入是为了避免循环依赖 |
| 测试困难 | 难以 mock 依赖 |
| 生命周期不清晰 | 不知道对象何时创建/销毁 |
| 资源泄漏风险 | 多个实例可能创建多个连接池 |
涉及文件
api/dependencies.py- 依赖获取domain/aigc/shared/component_factory.py- 组件工厂- 各个 Engine 类的
_get_xxx_service()方法
建议方案
统一依赖注入容器:
class Container:
_instances = {}
@classmethod
def get(cls, service_class: Type[T]) -> T:
if service_class not in cls._instances:
cls._instances[service_class] = cls._create(service_class)
return cls._instances[service_class]
@classmethod
def _create(cls, service_class):
# 根据类型创建实例,自动解析依赖
pass
或使用现有框架:
dependency-injector库- FastAPI 的
Depends系统
总结:优先级建议
| 优先级 | 问题 | 原因 |
|---|---|---|
| 🔴 高 | 数据库双端访问 | 数据一致性风险最大 |
| 🔴 高 | ppid 混乱 | 影响所有 AIGC 调用 |
| 🟡 中 | 图片 Base64 传输 | 性能问题,但功能正常 |
| 🟡 中 | Prompt 拼接 | 维护困难,但不影响功能 |
| 🟢 低 | 工具使用方式 | 代码质量问题,可渐进改进 |
6. 临时文件堆积问题
现状
每次 API 请求都会在 result/ 目录下创建新的子目录,保存中间产物:
$ du -sh result/
943M result/
$ ls -1 result/ | wc -l
6931
6931 个目录,占用 943MB 磁盘空间!
问题
| 问题 | 影响 |
|---|---|
| 磁盘空间浪费 | 近 1GB 的临时文件 |
| 无清理机制 | 文件只增不减 |
| 目录数量过多 | 影响文件系统性能 |
| 敏感信息泄露风险 | prompt、生成内容可能包含敏感数据 |
涉及文件
utils/file_io.py-OutputManager类- 每次请求创建:
result/api_request-{timestamp}-{uuid}/
建议方案
- 定期清理: 添加定时任务清理 7 天前的目录
- 按需保存: 只在调试模式下保存中间产物
- 内存缓存: 中间结果保存在内存,不落盘
- 统一存储: 重要结果上传到 S3,本地只做临时缓存
7. 配置分散问题
现状
配置文件分散在多个 JSON 文件中:
config/
├── ai_model.json # AI 模型配置
├── content_gen.json # 内容生成配置 (含 prompt 路径)
├── topic_gen.json # 选题生成配置 (含 prompt 路径)
├── poster_gen.json # 海报生成配置
├── database.json # 数据库配置
├── paths.json # 路径配置
├── resource.json # 资源配置
├── system.json # 系统配置
└── cookies.json # Cookie 配置
问题
| 问题 | 影响 |
|---|---|
| 配置分散 | 9 个配置文件,难以统一管理 |
| 路径嵌套 | 配置中引用其他配置路径 |
| 环境切换困难 | 没有 dev/prod 环境区分 |
| 敏感信息明文 | 数据库密码、API Key 明文存储 |
建议方案
- 环境变量优先: 敏感信息从环境变量读取
- 配置合并: 合并为
config.yaml+ 环境覆盖 - 配置中心: 使用 Consul/Nacos 等配置中心
8. 错误处理不一致
现状
不同模块的错误处理方式不统一:
# 方式1: 返回 None
if not topics:
return None
# 方式2: 返回错误字典
return {"error": str(e)}
# 方式3: 抛出异常
raise ValueError(f"生成海报失败: {str(e)}")
# 方式4: 返回元组
return str(uuid.uuid4()), []
问题
| 问题 | 影响 |
|---|---|
| 调用方难以处理 | 需要检查多种错误格式 |
| 错误信息丢失 | 有些地方只返回 None |
| 日志不统一 | 有的记录,有的不记录 |
| 没有错误码 | 难以区分错误类型 |
建议方案
统一使用 Result 模式:
@dataclass
class Result(Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[str] = None
error_code: Optional[str] = None
4. Prompt 管理方案 (详细设计)
当前问题分析
config/content_gen.json
└── "content_system_prompt": "resource/prompt/generateContent/system.txt"
│
▼
resource/prompt/generateContent/system.txt (87 行硬编码的 prompt)
│
▼
utils/prompts.py PromptTemplate.format(**kwargs)
│
▼
api/services/prompt_builder.py 拼接变量
问题:
- Prompt 内容硬编码在
.txt文件中 - 变量占位符
{style_content}与代码强耦合 - 无版本管理,修改后无法回滚
- 无 A/B 测试能力
- 无效果追踪
推荐方案: YAML + 版本化 Prompt Registry
目录结构
prompts/
├── registry.yaml # Prompt 注册表
├── topic_generate/
│ ├── v1.0.0.yaml # 版本化的 prompt
│ ├── v1.1.0.yaml
│ └── latest -> v1.1.0.yaml # 软链接
├── content_generate/
│ ├── v1.0.0.yaml
│ └── latest -> v1.0.0.yaml
└── content_judge/
└── v1.0.0.yaml
Prompt 定义格式 (YAML)
# prompts/content_generate/v1.0.0.yaml
meta:
name: content_generate
version: "1.0.0"
description: "小红书风格内容生成"
author: "team"
created_at: "2024-12-08"
# 模型参数
model:
temperature: 0.3
top_p: 0.5
presence_penalty: 1.2
# 变量定义 (带类型和默认值)
variables:
style_content:
type: string
required: true
description: "风格描述"
demand_content:
type: string
required: true
description: "受众需求"
object_content:
type: string
required: true
description: "景区/产品信息"
product_content:
type: string
required: false
default: ""
description: "产品详情"
refer_content:
type: string
required: false
default: ""
description: "参考范文"
# System Prompt
system: |
你是景区小红书爆款文案策划,你将根据要求创作爆款文案。
## 标题创作规则
1. 必加1个emoji,标题字数18字以内
2. 有网感,结合所在地
3. 标题必须结合给出的标题参考格式进行高相似度仿写
## 正文创作规则
1. 以"你"这种有人味的人称代词视角创作
2. 不要出现"宝子们""姐妹们"这些很假的称呼
3. 直击用户痛点,有场景感
4. 分段+分点论述,巧用emoji
## 输出格式
```json
{
"title": "标题内容",
"content": "正文内容",
"tag": "#标签1 #标签2 ..."
}
User Prompt (使用 Jinja2 模板语法)
user: | 请根据以下材料创作一篇小红书文案:
【风格要求】 {{ style_content }}
【目标受众】 {{ demand_content }}
【景区/产品信息】 {{ object_content }}
{% if product_content %} 【产品详情】 {{ product_content }} {% endif %}
{% if refer_content %} 【参考范文】 {{ refer_content }} {% endif %}
#### Prompt Registry 实现
```python
# domain/prompt/registry.py
from pathlib import Path
from typing import Dict, Any, Optional
import yaml
from jinja2 import Template
from dataclasses import dataclass
@dataclass
class PromptConfig:
"""Prompt 配置"""
name: str
version: str
system: str
user: str
variables: Dict[str, Any]
model: Dict[str, float]
class PromptRegistry:
"""
Prompt 注册表
功能:
1. 加载和缓存 prompt 配置
2. 版本管理 (latest, v1.0.0, v1.1.0)
3. 变量验证
4. 模板渲染
"""
def __init__(self, prompts_dir: str = "prompts"):
self.prompts_dir = Path(prompts_dir)
self._cache: Dict[str, PromptConfig] = {}
def get(self, name: str, version: str = "latest") -> PromptConfig:
"""
获取 prompt 配置
Args:
name: prompt 名称 (如 "content_generate")
version: 版本号 (如 "v1.0.0" 或 "latest")
"""
cache_key = f"{name}:{version}"
if cache_key not in self._cache:
self._cache[cache_key] = self._load(name, version)
return self._cache[cache_key]
def render(self, name: str, context: Dict[str, Any],
version: str = "latest") -> tuple[str, str]:
"""
渲染 prompt
Returns:
(system_prompt, user_prompt)
"""
config = self.get(name, version)
# 验证必填变量
self._validate_variables(config, context)
# 渲染模板
system = config.system # system 通常不需要变量
user = Template(config.user).render(**context)
return system, user
def _load(self, name: str, version: str) -> PromptConfig:
"""加载 prompt 配置文件"""
if version == "latest":
# 读取 latest 软链接指向的文件
prompt_path = self.prompts_dir / name / "latest.yaml"
if prompt_path.is_symlink():
prompt_path = prompt_path.resolve()
elif not prompt_path.exists():
# 找最新版本
versions = sorted(
(self.prompts_dir / name).glob("v*.yaml"),
reverse=True
)
if versions:
prompt_path = versions[0]
else:
prompt_path = self.prompts_dir / name / f"{version}.yaml"
if not prompt_path.exists():
raise FileNotFoundError(f"Prompt not found: {name}:{version}")
with open(prompt_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
return PromptConfig(
name=data['meta']['name'],
version=data['meta']['version'],
system=data['system'],
user=data['user'],
variables=data.get('variables', {}),
model=data.get('model', {})
)
def _validate_variables(self, config: PromptConfig, context: Dict[str, Any]):
"""验证变量"""
for var_name, var_config in config.variables.items():
if var_config.get('required', False) and var_name not in context:
raise ValueError(f"Missing required variable: {var_name}")
def list_versions(self, name: str) -> list[str]:
"""列出所有版本"""
prompt_dir = self.prompts_dir / name
if not prompt_dir.exists():
return []
versions = []
for f in prompt_dir.glob("v*.yaml"):
versions.append(f.stem)
return sorted(versions, reverse=True)
使用示例
# 使用新的 Prompt Registry
registry = PromptRegistry("prompts")
# 渲染 prompt
system, user = registry.render(
name="content_generate",
context={
"style_content": "攻略风格,实用性强",
"demand_content": "亲子家庭,周末出游",
"object_content": "天津冒险湾主题乐园...",
"product_content": "门票299元/人",
},
version="latest" # 或指定 "v1.0.0"
)
# 获取模型参数
config = registry.get("content_generate")
model_params = config.model # {"temperature": 0.3, ...}
优势
| 特性 | 说明 |
|---|---|
| 版本管理 | 每个版本独立文件,可回滚 |
| 变量验证 | 自动检查必填变量 |
| 模板语法 | Jinja2 支持条件、循环 |
| 模型参数绑定 | prompt 和模型参数一起管理 |
| 缓存 | 加载后缓存,避免重复 IO |
| A/B 测试 | 可指定不同版本测试效果 |
下一步行动
已确认的改进方向
| # | 问题 | 方案 |
|---|---|---|
| 1 | 数据库双端访问 | Python 不访问数据库,改为接口传输 |
| 2 | 图片 Base64 传输 | 改为 URL/路径引用 |
| 3 | ppid 混乱 | 放弃 ppid,Java 直接传完整数据 |
| 4 | Prompt 管理 | YAML + 版本化 Registry |
| 5 | 依赖注入 | 统一容器模式 |
| 6 | 临时文件堆积 | 添加清理机制 |
| 7 | 配置分散 | 合并 + 环境变量 |
| 8 | 错误处理不一致 | 统一 Result 模式 |
优先级排序
-
🔴 高优先级 (影响架构)
- 数据库访问改接口
- 图片传输改 URL
- 放弃 ppid
-
🟡 中优先级 (影响维护)
- Prompt Registry
- 错误处理统一
- 临时文件清理
-
🟢 低优先级 (代码质量)
- 依赖注入优化
- 配置合并
9. 巨型文件问题
现状
部分文件代码量过大,难以维护:
| 文件 | 行数 | 问题 |
|---|---|---|
demo_refactored_templates.py |
4219 | 示例/废弃代码? |
poster_template.py |
3421 | 根目录下的模板文件 |
api/services/poster.py |
3031 | 海报服务,职责过多 |
poster/templates/vibrant_template.py |
1756 | 单个模板文件过大 |
api/services/database_service.py |
1054 | 数据库服务 |
core/xhs_spider/apis/xhs_pc_apis.py |
1019 | 小红书 API |
问题
| 问题 | 影响 |
|---|---|
| 难以理解 | 3000+ 行代码难以阅读 |
| 难以测试 | 职责混杂,难以单元测试 |
| 合并冲突 | 多人修改同一文件容易冲突 |
| 废弃代码 | 根目录下的 poster_template.py 和 demo_*.py 可能是废弃代码 |
建议方案
- 拆分大文件:
poster.py拆分为多个模块 (已在新架构中完成) - 清理废弃代码: 删除根目录下的
poster_template.py,demo_*.py - 单一职责: 每个类/模块只做一件事
10. 重复代码问题
现状
存在多处重复或相似的代码:
# 目录重复
/root/TravelContentCreator/document/ # 旧位置
/root/TravelContentCreator/core/document/ # 新位置 (重复)
# 文件重复
./document/content_transformer.py
./core/document/content_transformer.py
./document/content_integrator.py
./core/document/content_integrator.py
问题
| 问题 | 影响 |
|---|---|
| 不知道用哪个 | 两个位置都有相同文件 |
| 修改遗漏 | 改了一个忘了另一个 |
| 导入混乱 | from document.xxx vs from core.document.xxx |
建议方案
- 确定唯一位置
- 删除重复目录
- 更新所有 import
11. API 路由分散问题
现状
API 路由分散在多个文件中,功能有重叠:
api/routers/
├── aigc.py # 新的统一 AIGC API
├── tweet.py # 旧的选题/内容 API (470 行)
├── poster.py # 旧的海报 API
├── data.py # 数据查询 API
├── document.py # 文档处理 API
├── integration.py # 集成 API
├── content_integration.py # 内容集成 API
└── prompt.py # 提示词 API
问题
| 问题 | 影响 |
|---|---|
| 功能重叠 | aigc.py 和 tweet.py/poster.py 功能重复 |
| 命名不一致 | tweet vs content, integration vs content_integration |
| API 版本混乱 | 没有清晰的 v1/v2 区分 |
建议方案
- 统一使用
/api/v2/aigc/*作为新入口 - 旧 API 标记为 deprecated
- 逐步迁移后删除旧路由
12. 日志配置问题
现状
每个文件都单独配置 logger:
# 几乎每个 .py 文件都有这行
logger = logging.getLogger(__name__)
但没有统一的日志配置:
- 日志格式不统一
- 日志级别分散控制
- 没有日志轮转
- 没有结构化日志
建议方案
# 统一日志配置
LOGGING_CONFIG = {
"version": 1,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
},
"json": {
"class": "pythonjsonlogger.jsonlogger.JsonFormatter"
}
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "default"},
"file": {"class": "logging.handlers.RotatingFileHandler", ...}
},
"root": {"level": "INFO", "handlers": ["console", "file"]}
}
完整问题清单
| # | 问题 | 严重程度 | 状态 |
|---|---|---|---|
| 1 | 数据库双端访问 | 🔴 高 | ✅ V2 引擎已解决 |
| 2 | 图片 Base64 传输 | 🟡 中 | ✅ V2 引擎支持 URL |
| 3 | ppid 混乱 | 🔴 高 | ✅ 已废弃,Java 端传完整对象 |
| 4 | Prompt 管理分散 | 🟡 中 | ✅ PromptRegistry 已实现 |
| 5 | 依赖注入不统一 | 🟢 低 | ✅ Container 已实现 |
| 6 | 临时文件堆积 | 🟡 中 | ✅ 清理脚本已创建 |
| 7 | 配置分散 | 🟢 低 | ✅ UnifiedConfig 已实现 |
| 8 | 错误处理不一致 | 🟡 中 | ✅ 统一异常类已定义 |
| 9 | 巨型文件 | 🟡 中 | 部分已拆分 |
| 10 | 重复代码/目录 | 🟡 中 | ✅ document 已统一 |
| 11 | API 路由分散 | 🟢 低 | 新架构已统一 |
| 12 | 日志配置缺失 | 🟢 低 | ✅ logging_config 已实现 |
详细实施记录见 REFACTORING_IMPLEMENTATION.md