TravelContentCreator/docs/TECHNICAL_DEBT.md

25 KiB
Raw Blame History

技术债务分析报告

本文档总结了 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 - 每个引擎都要处理 ppid
  • infrastructure/database/repositories/product_package_repository.py
  • AigcCompatibilityServiceImpl.java

建议方案

统一入口点:

前端/调用方 ──ppid──> Java 后端 ──解析──> sid + pid ──> Python
  • 所有 ppid 解析在 Java 端完成
  • Python 端只接收已解析的 scenic_spot_idproduct_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}/

建议方案

  1. 定期清理: 添加定时任务清理 7 天前的目录
  2. 按需保存: 只在调试模式下保存中间产物
  3. 内存缓存: 中间结果保存在内存,不落盘
  4. 统一存储: 重要结果上传到 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 明文存储

建议方案

  1. 环境变量优先: 敏感信息从环境变量读取
  2. 配置合并: 合并为 config.yaml + 环境覆盖
  3. 配置中心: 使用 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 拼接变量

问题:

  1. Prompt 内容硬编码在 .txt 文件中
  2. 变量占位符 {style_content} 与代码强耦合
  3. 无版本管理,修改后无法回滚
  4. 无 A/B 测试能力
  5. 无效果追踪

推荐方案: 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 混乱 放弃 ppidJava 直接传完整数据
4 Prompt 管理 YAML + 版本化 Registry
5 依赖注入 统一容器模式
6 临时文件堆积 添加清理机制
7 配置分散 合并 + 环境变量
8 错误处理不一致 统一 Result 模式

优先级排序

  1. 🔴 高优先级 (影响架构)

    • 数据库访问改接口
    • 图片传输改 URL
    • 放弃 ppid
  2. 🟡 中优先级 (影响维护)

    • Prompt Registry
    • 错误处理统一
    • 临时文件清理
  3. 🟢 低优先级 (代码质量)

    • 依赖注入优化
    • 配置合并

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.pydemo_*.py 可能是废弃代码

建议方案

  1. 拆分大文件: poster.py 拆分为多个模块 (已在新架构中完成)
  2. 清理废弃代码: 删除根目录下的 poster_template.py, demo_*.py
  3. 单一职责: 每个类/模块只做一件事

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

建议方案

  1. 确定唯一位置
  2. 删除重复目录
  3. 更新所有 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.pytweet.py/poster.py 功能重复
命名不一致 tweet vs content, integration vs content_integration
API 版本混乱 没有清晰的 v1/v2 区分

建议方案

  1. 统一使用 /api/v2/aigc/* 作为新入口
  2. 旧 API 标记为 deprecated
  3. 逐步迁移后删除旧路由

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