# 技术债务分析报告 > 本文档总结了 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 解码 → 处理 ```python # poster.py L266 image_bytes = base64.b64decode(first_image_base64) ``` ```java // 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 引用** ```json { "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 | ### 问题 ```python # 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') ``` ```java // 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_id` 和 `product_id` - 消除 Python 端的 ppid 解析逻辑 --- ## 4. Prompt 拼接方式问题 ### 现状 Prompt 构建分散在多个地方,使用字符串拼接和模板文件混合: ```python # 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 ) ``` ```python # 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 管理** ```python 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 配置化** ```yaml # prompts.yaml topic_generate: version: "1.2.0" system: | 你是一个旅游营销专家... user: | 请为 {scenic_spot} 生成 {num_topics} 个选题... variables: - scenic_spot: required - num_topics: default=5 ``` --- ## 5. 工具使用方式问题 ### 现状 各种工具类和服务的使用方式不统一: ```python # 方式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()` 方法 ### 建议方案 **统一依赖注入容器:** ```python 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/` 目录下创建新的子目录,保存中间产物: ```bash $ 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. 错误处理不一致 ### 现状 不同模块的错误处理方式不统一: ```python # 方式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` 模式: ```python @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) ```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) ``` #### 使用示例 ```python # 使用新的 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 模式 | ### 优先级排序 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.py` 和 `demo_*.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.py` 和 `tweet.py`/`poster.py` 功能重复 | | **命名不一致** | `tweet` vs `content`, `integration` vs `content_integration` | | **API 版本混乱** | 没有清晰的 v1/v2 区分 | ### 建议方案 1. 统一使用 `/api/v2/aigc/*` 作为新入口 2. 旧 API 标记为 deprecated 3. 逐步迁移后删除旧路由 --- ## 12. 日志配置问题 ### 现状 每个文件都单独配置 logger: ```python # 几乎每个 .py 文件都有这行 logger = logging.getLogger(__name__) ``` 但没有统一的日志配置: - 日志格式不统一 - 日志级别分散控制 - 没有日志轮转 - 没有结构化日志 ### 建议方案 ```python # 统一日志配置 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](./REFACTORING_IMPLEMENTATION.md)