# Phase F.4：AI 集成

日期：2026-04-04
状态：待开始
依赖：F.3（待完成）
前置：services/forum_ai_service.py

---

## 1. 本阶段目的

为 Jarvis Forum 集成 AI 能力：

- AI 自动回复
- 帖子摘要生成
- 智能分类打标
- Agent 自主发帖

---

## 2. Forum AI Service

### 2.1 服务架构

```
┌─────────────────────────────────────────────────────────────┐
│                    ForumAIService                           │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │
│  │ AutoReply   │  │  Summary    │  │  SmartTagging   │   │
│  │   Service   │  │  Service    │  │    Service      │   │
│  └──────┬──────┘  └──────┬──────┘  └────────┬────────┘   │
│         │                 │                   │            │
│         └────────────────┼───────────────────┘            │
│                          │                                │
│              ┌───────────┴───────────┐                   │
│              │    LLM Service        │                   │
│              │  (复用 Agent LLM)     │                   │
│              └───────────────────────┘                   │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 核心服务

```python
class ForumAIService:
    """论坛 AI 服务"""
    
    def __init__(
        self,
        llm_service: LLMService,
        config: ForumAIConfig,
    ):
        self.llm = llm_service
        self.config = config
    
    # === 自动回复 ===
    async def generate_auto_reply(
        self,
        post: ForumPost,
        replies: List[ForumReply],
    ) -> Optional[str]:
        """生成自动回复"""
        if not self.config.auto_reply_enabled:
            return None
        
        # 检查是否需要回复
        if not self._should_auto_reply(post):
            return None
        
        # 构建上下文
        context = self._build_reply_context(post, replies)
        
        # 生成回复
        prompt = self._build_reply_prompt(context)
        response = await self.llm.agenerate(prompt)
        
        return response.content if response else None
    
    def _should_auto_reply(self, post: ForumPost) -> bool:
        """判断是否应该自动回复"""
        # 指令类帖子不自动回复
        if post.category == "instruction":
            return False
        
        # 有 AI 回复的不重复回复
        if any(r.is_ai_reply for r in post.replies):
            return False
        
        # 检查时间窗口
        if post.last_reply_at:
            hours_since = (datetime.utcnow() - post.last_reply_at).total_seconds() / 3600
            if hours_since < self.config.auto_reply_min_hours:
                return False
        
        return True
    
    def _build_reply_context(
        self,
        post: ForumPost,
        replies: List[ForumReply],
    ) -> str:
        """构建回复上下文"""
        context = f"""## 帖子信息
标题: {post.title}
内容: {post.content[:500]}...

## 回复列表
"""
        for reply in replies[-5:]:  # 最近 5 条
            context += f"- {reply.user_id}: {reply.content[:200]}...\n"
        
        return context
    
    def _build_reply_prompt(self, context: str) -> str:
        """构建回复提示词"""
        return f"""你是一个友好的社区助手。请根据以下帖子内容，生成一条有帮助的回复。

{context}

要求:
1. 回复要友好、专业、有帮助
2. 不要重复已有的观点
3. 如果是问题，尽量给出建设性的建议
4. 回复长度控制在 100-300 字
5. 不要使用 Markdown 格式，直接输出文本

回复:"""
    
    # === 摘要生成 ===
    async def generate_summary(
        self,
        content: str,
        max_length: int = 200,
    ) -> str:
        """生成帖子摘要"""
        if len(content) <= max_length:
            return content
        
        prompt = f"""请为以下内容生成一个简洁的摘要，不超过 {max_length} 字。

内容:
{content}

摘要:"""
        
        response = await self.llm.agenerate(prompt)
        return response.content if response else content[:max_length]
    
    # === 智能打标 ===
    async def suggest_tags(
        self,
        title: str,
        content: str,
        existing_tags: List[str],
    ) -> List[str]:
        """智能推荐标签"""
        prompt = f"""请根据以下帖子内容，推荐 3-5 个最合适的标签。

标题: {title}
内容: {content[:1000]}...

已有标签: {', '.join(existing_tags) if existing_tags else '无'}

要求:
1. 标签要简洁，2-4 个字
2. 选择最相关的标签
3. 如果已有标签合适可以保留
4. 只输出标签，用逗号分隔，不要其他内容

标签:"""
        
        response = await self.llm.agenerate(prompt)
        if not response:
            return existing_tags
        
        # 解析标签
        tags = [t.strip() for t in response.content.split(",")]
        return tags[:5]  # 最多 5 个
    
    # === 智能分类 ===
    async def classify_category(
        self,
        title: str,
        content: str,
    ) -> str:
        """智能分类"""
        categories = ["instruction", "discussion", "question", "praise", "bug", "other"]
        
        prompt = f"""请为以下帖子选择一个最合适的分类。

标题: {title}
内容: {content[:500]}...

可选分类:
- instruction: 指令/任务请求
- discussion: 讨论/分享
- question: 问题/求助
- praise: 表扬/感谢
- bug: Bug 反馈
- other: 其他

请直接输出分类名称，不要其他内容。

分类:"""
        
        response = await self.llm.agenerate(prompt)
        if not response:
            return "other"
        
        category = response.content.strip().lower()
        if category not in categories:
            return "other"
        
        return category
```

---

## 3. Summary Service

### 3.1 服务实现

```python
class SummaryService:
    """帖子摘要服务"""
    
    def __init__(self, cache: ForumCache):
        self.cache = cache
    
    async def get_post_summary(
        self,
        post_id: str,
        post: ForumPost,
    ) -> str:
        """获取帖子摘要（带缓存）"""
        cache_key = f"summary:{post_id}"
        
        # 尝试从缓存获取
        cached = await self.cache.get(cache_key)
        if cached:
            return cached
        
        # 生成摘要
        summary = await self._generate_summary(post)
        
        # 存入缓存（1 小时）
        await self.cache.set(cache_key, summary, ttl=3600)
        
        return summary
    
    async def get_thread_summary(
        self,
        post: ForumPost,
        replies: List[ForumReply],
    ) -> str:
        """获取帖子串摘要（主帖+回复摘要）"""
        summaries = [await self.get_post_summary(post.id, post)]
        
        # 汇总回复要点
        if len(replies) > 0:
            summary_prompt = f"""请总结以下回复的核心观点，用 50 字以内概括。

回复列表:
{self._format_replies(replies[:10])}

总结:"""
            
            response = await self.llm.agenerate(summary_prompt)
            if response:
                summaries.append(f"回复要点: {response.content}")
        
        return "\n\n".join(summaries)
    
    async def invalidate_summary(self, post_id: str) -> None:
        """清除摘要缓存"""
        await self.cache.invalidate(f"summary:{post_id}")
```

---

## 4. Agent 自主发帖

### 4.1 Agent Forum 工具

```python
# agents/tools/forum_tools.py
from typing import List, Optional


class ForumTools:
    """Forum Agent 工具集"""
    
    def __init__(self, forum_service: ForumService, ai_service: ForumAIService):
        self.forum = forum_service
        self.ai = ai_service
    
    @tool
    async def create_forum_post(
        title: str,
        content: str,
        board_id: Optional[str] = None,
        category: Optional[str] = None,
        tags: Optional[List[str]] = None,
    ) -> dict:
        """创建论坛帖子
        
        参数:
        - title: 帖子标题
        - content: 帖子内容
        - board_id: 板块 ID（可选）
        - category: 分类（可选）
        - tags: 标签列表（可选）
        """
        data = ForumPostCreate(
            title=title,
            content=content,
            board_id=board_id,
            category=category,
            tags=tags or [],
        )
        post = await self.forum.create_post(agent_id=AGENT_ID, data=data)
        return {"post_id": post.id, "title": post.title}
    
    @tool
    async def reply_to_post(
        post_id: str,
        content: str,
    ) -> dict:
        """回复帖子
        
        参数:
        - post_id: 帖子 ID
        - content: 回复内容
        """
        data = ForumReplyCreate(content=content)
        reply = await self.forum.create_reply(
            post_id=post_id,
            agent_id=AGENT_ID,
            data=data,
        )
        return {"reply_id": reply.id, "floor": reply.floor}
    
    @tool
    async def search_forum_posts(
        query: str,
        board_id: Optional[str] = None,
        category: Optional[str] = None,
        limit: int = 10,
    ) -> List[dict]:
        """搜索论坛帖子
        
        参数:
        - query: 搜索关键词
        - board_id: 限定板块（可选）
        - category: 限定分类（可选）
        - limit: 返回数量（默认 10）
        """
        posts = await self.forum.search_posts(
            query=query,
            board_id=board_id,
            category=category,
            limit=limit,
        )
        return [
            {"id": p.id, "title": p.title, "summary": p.content[:100]}
            for p in posts
        ]
    
    @tool
    async def get_forum_trending(
        board_id: Optional[str] = None,
        limit: int = 5,
    ) -> List[dict]:
        """获取热门帖子
        
        参数:
        - board_id: 板块 ID（可选）
        - limit: 返回数量（默认 5）
        """
        posts = await self.forum.get_trending(
            board_id=board_id,
            limit=limit,
        )
        return [
            {
                "id": p.id,
                "title": p.title,
                "reply_count": p.reply_count,
                "view_count": p.view_count,
            }
            for p in posts
        ]
```

### 4.2 Agent 配置

```python
# agents/prompts/forum_agent.py
FORUM_AGENT_PROMPT = """你是一个活跃的社区成员，可以帮助用户解决问题和参与讨论。

## 你的能力
1. 在论坛发帖分享信息或见解
2. 回复其他用户的帖子
3. 搜索论坛内容
4. 查看热门帖子

## 行为规则
1. 只在有帮助时才发帖/回复，不要刷屏
2. 回复要专业、有建设性
3. 如果用户的问题已经解决，不要重复回答
4. 遇到 Bug 或问题可以主动发帖提醒
5. 定期查看论坛，如果有重要帖子可以参与讨论

## 当前时间
{current_time}

## 用户信息
用户名: {username}
积分: {forum_score}
"""

# Agent 可用的 Forum 工具
FORUM_TOOLS = [
    create_forum_post,
    reply_to_post,
    search_forum_posts,
    get_forum_trending,
]
```

---

## 5. 定时任务

### 5.1 自动回复任务

```python
# tasks/forum_auto_reply.py
from apscheduler.schedulers.asyncio import AsyncIOScheduler


async def auto_reply_task():
    """自动回复任务"""
    # 获取需要回复的帖子
    posts = await forum_service.get_pending_posts(
        min_age_hours=settings.auto_reply_min_hours,
        limit=10,
    )
    
    for post in posts:
        replies = await forum_service.get_replies(post.id)
        
        # 生成回复
        response = await ai_service.generate_auto_reply(post, replies)
        
        if response:
            # 发布回复
            await forum_service.create_reply(
                post_id=post.id,
                agent_id=AGENT_ID,
                data=ForumReplyCreate(content=response),
            )
            
            # 更新回复计数
            await forum_service.increment_reply_count(post.id)


def setup_forum_scheduler(scheduler: AsyncIOScheduler):
    """配置论坛定时任务"""
    # 每小时检查一次自动回复
    scheduler.add_job(
        auto_reply_task,
        "interval",
        hours=1,
        id="forum_auto_reply",
    )
```

---

## 6. 配置项

### 6.1 AI 配置

```python
class ForumAIConfig:
    """Forum AI 配置"""
    
    # 自动回复
    auto_reply_enabled: bool = True
    auto_reply_min_hours: int = 24  # 帖子发布 N 小时后才自动回复
    auto_reply_max_per_day: int = 10  # 每天最多自动回复 N 条
    
    # 摘要生成
    summary_enabled: bool = True
    summary_max_length: int = 200
    summary_cache_ttl: int = 3600  # 缓存 1 小时
    
    # 智能打标
    smart_tagging_enabled: bool = True
    smart_tagging_max_tags: int = 5
    
    # 智能分类
    smart_classification_enabled: bool = True


# settings.py
class Settings(BaseSettings):
    # Forum AI
    forum_ai_auto_reply: bool = True
    forum_ai_summary: bool = True
    forum_ai_smart_tagging: bool = True
```

---

## 7. API 端点

### 7.1 AI 相关端点

```python
@router.post("/posts/{post_id}/generate-summary")
async def generate_post_summary(
    post_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """生成帖子摘要"""
    result = await db.execute(
        select(ForumPost).where(ForumPost.id == post_id)
    )
    post = result.scalar_one_or_none()
    
    if not post:
        raise HTTPException(status_code=404, detail="帖子不存在")
    
    ai_service = ForumAIService(llm_service, config)
    summary = await ai_service.generate_summary(post.content)
    
    return {"summary": summary}


@router.post("/posts/suggest-tags")
async def suggest_post_tags(
    title: str,
    content: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """推荐帖子标签"""
    ai_service = ForumAIService(llm_service, config)
    tags = await ai_service.suggest_tags(title, content, [])
    
    return {"tags": tags}


@router.post("/posts/classify")
async def classify_post(
    title: str,
    content: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """分类帖子"""
    ai_service = ForumAIService(llm_service, config)
    category = await ai_service.classify_category(title, content)
    
    return {"category": category}


@router.get("/posts/{post_id}/ai-status")
async def get_ai_status(
    post_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """获取帖子 AI 状态"""
    result = await db.execute(
        select(ForumPost).where(ForumPost.id == post_id)
    )
    post = result.scalar_one_or_none()
    
    if not post:
        raise HTTPException(status_code=404, detail="帖子不存在")
    
    # 检查 AI 回复状态
    has_ai_reply = any(r.is_ai_reply for r in post.replies)
    summary_cached = await cache.get(f"summary:{post_id}")
    
    return {
        "has_ai_reply": has_ai_reply,
        "summary_available": bool(summary_cached),
    }
```

---

## 8. 实现步骤

| 步骤 | 任务 | 优先级 |
|------|------|--------|
| 1 | 创建 ForumAIService | 🟢 高 |
| 2 | 实现自动回复 | 🟢 高 |
| 3 | 实现摘要生成 | 🟡 中 |
| 4 | 实现智能打标 | 🟡 中 |
| 5 | 实现智能分类 | 🟡 中 |
| 6 | 创建 ForumTools | 🟢 高 |
| 7 | 配置定时任务 | 🟡 中 |
| 8 | 扩展 API 端点 | 🟡 中 |
| 9 | 单元测试 | 🟡 中 |

---

## 9. 核心文件变更

| 文件 | 变更 |
|------|------|
| `services/forum_ai_service.py` | 新增 |
| `services/summary_service.py` | 新增 |
| `agents/tools/forum_tools.py` | 新增 |
| `agents/prompts/forum_agent.py` | 新增 |
| `tasks/forum_auto_reply.py` | 新增 |
| `routers/forum.py` | 扩展 AI 端点 |

---

## 10. 工作量估算

| 任务 | 工作量 |
|------|--------|
| ForumAIService | 1 天 |
| 自动回复 | 1 天 |
| 摘要/打标/分类 | 1 天 |
| ForumTools | 1 天 |
| 定时任务 | 0.5 天 |
| API 端点 | 0.5 天 |
| 单元测试 | 0.5 天 |
| **总计** | **5.5 天** |

---

## 11. 验收标准

- [ ] ForumAIService 可正常调用 LLM
- [ ] 自动回复功能正常工作
- [ ] 摘要生成功能正常
- [ ] 智能打标推荐准确
- [ ] 智能分类推荐准确
- [ ] ForumTools 可被 Agent 调用
- [ ] 定时任务正常执行
- [ ] API 端点正常工作
- [ ] 单元测试覆盖核心逻辑
