项目:阅见(VistaRead)—— 基于大模型的深度交互式阅读平台
技术栈:Vue 3 · Spring Boot · FastAPI · 通义千问 · ChromaDB · SSE
难度:中级 | 阅读时长:约 18 分钟
关键词:大模型 Agent、OpenAI 兼容 API、FastAPI 中台、多智能体、流式对话 SSE


日期: 2026年4月20日——2026年5月10日
项目: 阅见:基于大模型 的交互式小说阅读平台

前言:为什么需要「AI 中台」而不是前端直调大模型?

在实训初期,团队曾讨论过「Vue 页面直接调用通义千问 API」的方案。该方案开发快,但很快暴露出三类问题:

  1. 密钥泄露风险:API Key 无法安全放在浏览器端;
  2. 业务与 AI 耦合:角色记忆、剧情状态、用户鉴权无法统一编排;
  3. 难以扩展 Agent:WorldAgent、RoleAgent 等多智能体逻辑不适合堆在前端或 Java 业务层。

最终,「阅见」采用 Vue → Java 网关 → Python AI 中台 → 大模型 API 的四段式链路。Java 负责鉴权与持久化,Python 负责 Agent 编排与向量检索,前端只关心 /api/* 业务接口。

本文你将学到:

  • 三层 AI 架构的设计动机与职责边界
  • ai-service/.env 环境变量如何驱动 Agent 启动
  • LlmClientRoleAgentWorldAgent 的配置与协作方式
  • Java AiBridgeService / AiStreamingService 如何实现同步与 SSE 流式转发
  • 本地联调步骤、健康检查方法与 5 个常见踩坑 FAQ

目录

一、整体架构与设计原则

1.1 四层调用链路

1.2 为什么前端不直连 Python

1.3 核心模块职责一览

二、环境配置与快速启动

2.1 依赖安装

2.2 .env 环境变量详解

2.3 四步启动与验证

三、Python AI 中台:Agent 配置核心

3.1 FastAPI 入口与 lifespan 初始化

3.2 统一 LLM 客户端 LlmClient

3.3 Agent 类设计与动态装配

3.4 角色对话完整处理流程

四、Java 桥接层与前端调用

4.1 同步转发:AiBridgeService

4.2 流式转发:AiStreamingService

4.3 前端 SSE 消费示例

五、API 接口清单

六、踩坑记录与 FAQ

七、经验总结与后续计划


一、整体架构与设计原则

1.1 四层调用链路

以一次 角色流式对话 为例,请求依次经过四层:

通义千问 API Python 8000 Java 8080 浏览器 Vue 通义千问 API Python 8000 Java 8080 浏览器 Vue Vite 代理 /api → 8080 AiStreamingService SSE 转发 POST /api/ai/chat/stream POST /v1/chat/stream PromptEnhancer 检索记忆 构建 RoleAgent / 组装 Prompt POST /chat/completions (stream=true) SSE delta (content / reasoning) data: {...}\n\n 原样转发 SSE

静态部署关系如下:

┌─────────────┐     /api/*      ┌──────────────────┐    /v1/*     ┌─────────────────────┐
│  Vue 前端    │ ──────────────→ │  Spring Boot 8080 │ ──────────→ │  FastAPI AI 中台 8000 │
│  (5173)     │   Vite 代理     │  AiBridgeService  │  RestTemplate│  main.py            │
└─────────────┘                 │  AiStreamingService│  SSE 转发   │  RoleAgent/WorldAgent│
                                └──────────────────┘              └──────────┬──────────┘
                                         │                                    │
                                         ↓                                    ↓
                                   MySQL 业务数据                      ChromaDB + 通义千问

1.2 为什么前端不直连 Python

方案 优点 缺点 本项目选择
前端直调 LLM 链路短 Key 暴露、无鉴权、难持久化 ❌ 不采用
Java 内嵌 LLM SDK 单服务部署 Java 生态 Agent 库弱、Prompt 迭代慢 ❌ 不采用
独立 Python 中台 Agent 灵活、向量库原生、OpenAI 兼容 多一个进程 采用

前端代理配置如下,所有 AI 请求必须走 Java

// frontend/vite.config.js
server: {
  port: 5173,
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
    },
  },
},

Java 侧声明 AI 中台地址:

# backend/src/main/resources/application.yml
vistaread:
  ai-service:
    base-url: http://localhost:8000

1.3 核心模块职责一览

层级 关键文件 职责
前端 AiChat.vuePlotBranch.vue 组装请求体、消费 SSE 流
Java AiFeatureControllerPlotController 鉴权、转发、离线降级
Java AiBridgeServiceAiStreamingService 同步 REST / SSE 桥接
Python main.py 路由注册、lifespan 初始化、对话入口
Python llm_client.py OpenAI 兼容 HTTP 封装
Python role_agent.pyworld_agent.py 角色 / 世界智能体
Python orchestrator.py 多 Agent 编排
Python modules/prompt_enhancer.py 记忆检索 + Prompt 注入

二、环境配置与快速启动

2.1 依赖安装

Python AI 中台(建议使用独立虚拟环境):

cd ai-service
conda create -n vistaread-ai python=3.10 -y
conda activate vistaread-ai
pip install -r requirements.txt

核心依赖:fastapiuvicornhttpxpython-dotenvdashscopechromadb

Java 后端Vue 前端

# 后端
cd backend && mvn spring-boot:run

# 前端
cd frontend && npm install && npm run dev

2.2 .env 环境变量详解

ai-service/.env 中配置(切勿提交到 Git):

变量名 是否必填 说明 示例值
LLM_API_BASE OpenAI 兼容端点 https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_API_KEY 对话模型密钥 sk-xxx
LLM_MODEL 对话模型名 qwen-plus
LLM_MODEL_WORLD 可选 世界 Agent 专用模型 默认同 LLM_MODEL
LLM_TIMEOUT 可选 HTTP 超时(秒) 120
DASHSCOPE_API_KEY 记忆功能必填 Embedding 密钥 可与 LLM_API_KEY 相同
EMBEDDING_MODEL 可选 向量模型 text-embedding-v2
CHROMA_DB_PATH 可选 向量库本地路径 ./chroma_db

配置加载逻辑——固定从 ai-service 目录读取,避免工作目录不一致:

# ai-service/main.py
load_dotenv(dotenv_path=Path(__file__).resolve().parent / ".env")

密钥降级策略(Embedding 与对话可共用同一 Key):

def _resolve_dashscope_api_key() -> str:
    key = _get_env("DASHSCOPE_API_KEY") or _get_env("LLM_API_KEY")
    if not key or key == "your-api-key-here":
        raise RuntimeError("未配置有效的 DASHSCOPE_API_KEY 或 LLM_API_KEY")
    return key

提示:记忆模块初始化失败时,服务仍会启动,但 /healthmemory_readyfalse,对话功能不受影响,只是无法检索历史记忆。

2.3 四步启动与验证

Step 1 — 启动 MySQL(端口 3306)并导入 schema.sql

Step 2 — 启动 AI 中台:

cd ai-service
python main.py
# 或:uvicorn main:app --host 0.0.0.0 --port 8000 --reload

Step 3 — 健康检查:

curl http://localhost:8000/health

期望返回:

{
  "status": "up",
  "service": "vistaread-ai",
  "memory_ready": true,
  "plot_index_ready": true,
  "plot_data_source": "Plot_development/data"
}

Step 4 — 测试同步对话接口:

curl -X POST http://localhost:8000/v1/chat/role \
  -H "Content-Type: application/json" \
  -d "{\"mode\":\"character\",\"roleName\":\"林黛玉\",\"message\":\"你好\",\"context\":\"红楼梦片段\"}"

若返回 JSON 含 "offline": falsereply 有内容,说明 Agent + LLM 链路已通。


三、Python AI 中台:Agent 配置核心

3.1 FastAPI 入口与 lifespan 初始化

main.py 是 AI 中台唯一入口,模块注释明确了服务定位:

"""
阅见 · Python 大模型中台(FastAPI)
当前已接入 /v1/chat/role 的 OpenAI 兼容 API 调用。
支持流式输出 /v1/chat/stream
"""

Agent 相关依赖在 lifespan集中初始化,而非在每次请求时 lazy load——这样可以避免首请求超时,也方便通过 /health 暴露就绪状态:

@asynccontextmanager
async def lifespan(app: FastAPI):
    global memory_manager, history_manager, prompt_enhancer
    try:
        embedding_service = EmbeddingService(api_key=_resolve_dashscope_api_key(), ...)
        chroma_store = ChromaMemoryStore(db_path=_get_env("CHROMA_DB_PATH", "./chroma_db"), ...)
        memory_manager = MemoryManager(embedding_service, chroma_store)
        history_manager = HistoryManager(memory_manager)
        prompt_enhancer = PromptEnhancer(memory_manager)
        memory_routes.memory_manager = memory_manager  # 注入路由模块
        app.state.memory_manager = memory_manager
        # PlotChromaStore / PlotKnowledgeIndexer 同理初始化 ...
    except Exception as e:
        print(f"⚠️ 警告: 记忆/剧情向量服务初始化失败: {e}")
        memory_manager = None  # 降级,不阻断服务
    yield

app = FastAPI(title="VistaRead AI Service", version="0.1.0", lifespan=lifespan)
app.include_router(memory_routes.router)
app.include_router(ppt_routes.router)

设计要点:路由在模块加载时注册,依赖在 lifespan 中注入——避免重复注册,也避免循环 import。

3.2 统一 LLM 客户端 LlmClient

所有 Agent 的大模型调用收敛到 llm_client.py,采用 OpenAI Chat Completions 兼容协议,换供应商只需改 .env

@dataclass(frozen=True)
class LlmSettings:
    api_base: str
    api_key: str
    model: str
    model_world: str      # 世界 Agent 可单独指定模型
    timeout_sec: float

def load_llm_settings() -> LlmSettings:
    base = (os.getenv("LLM_API_BASE")
            or "https://dashscope.aliyuncs.com/compatible-mode/v1").rstrip("/")
    model = os.getenv("LLM_MODEL") or "gpt-4o-mini"
    model_world = (os.getenv("LLM_MODEL_WORLD") or model).strip()
    ...

LlmClient.chat() 的三处关键逻辑:

  1. 自动补全 URL 为 .../chat/completions
  2. 支持 thinking: {type: enabled/disabled} 思考模式
  3. 失败时最多重试 2 次,指数退避
class LlmClient:
    def _url(self) -> str:
        url = self.settings.api_base.rstrip("/")
        if not url.endswith("/chat/completions"):
            url = f"{url}/chat/completions"
        return url

    def chat(self, messages, *, thinking_enabled=False, max_retries=2) -> str:
        payload = {"model": self.settings.model, "messages": messages, "stream": False}
        payload["thinking"] = {"type": "enabled" if thinking_enabled else "disabled"}
        # httpx POST + 重试 ...

说明main.py 中的 _call_chat_model() 是对话场景的薄封装,负责组装 system prompt(含角色 profile)、history 与 user message;WorldAgent 则直接使用 LlmClient

3.3 Agent 类设计与动态装配

Agent 按 职责拆分,而非一个大而全的 God Class:

剧情推演

PlotAdvanceIn

PlotOrchestrator

WorldAgent 编排

RoleAgent 行动

向量检索 PlotKnowledgeIndexer

对话场景

ChatRoleIn 请求

role_profiles.json 有匹配?

RoleAgent.respond

_call_chat_model 直连

RoleAgent — 角色级智能体,从 JSON profile 解析人设:

@dataclass
class RoleAgent:
    role_name: str
    profile_data: dict[str, Any]
    llm_caller: ChatModelCaller   # 注入模型调用函数,便于测试与替换
    thinking_enabled: bool | None = None
    history: list[dict[str, str]] = field(default_factory=list)

    def __post_init__(self) -> None:
        identity = self.profile_data.get("identity") or {}
        self.nickname = identity.get("nickname") or self.role_name
        self.motivation = (self.profile_data.get("motivation_goal") or {}).get("motivation") or ""

WorldAgent — 世界级智能体,负责事件推进与行动者调度:

class WorldAgent:
    def update_event(self, event, user_choice, history_text) -> str:
        prompt = P.UPDATE_EVENT.format(world_description=self.world_description, ...)
        return self.llm.chat([{"role": "user", "content": prompt}], temperature=0.5).strip()

    def decide_next_actor(self, role_names, history_text, event) -> str:
        # 调用 LLM 决定下一行动角色 ...

PlotOrchestrator — 编排器,组合 World + Role + 向量检索:

class PlotOrchestrator:
    def __init__(self, llm=None, llm_caller=None, *, knowledge_indexer=None, history_store=None):
        self.llm = llm or LlmClient()
        self._llm_caller = llm_caller
        self.knowledge_indexer = knowledge_indexer
        self.history_store = history_store

动态装配函数 _build_role_agent() 在运行时根据角色名查找 profile,找不到则返回 None 并降级:

def _build_role_agent(role_name: str, thinking_enabled=None) -> RoleAgent | None:
    profile = _find_role_profile(role_name)  # 支持别名匹配
    if not profile:
        return None
    return RoleAgent(
        role_name=role_name.strip(),
        profile_data=profile,
        llm_caller=_call_chat_model,
        thinking_enabled=thinking_enabled,
    )

3.4 角色对话完整处理流程

/v1/chat/role 的处理顺序可以概括为 「检索 → 装配 → 推理 → 持久化」

步骤 模块 动作
1 PromptEnhancer 从 ChromaDB 检索 top-k 相关记忆,注入 context
2 _build_role_agent 查找 profile,构建 RoleAgent
3 RoleAgent.respond / _call_chat_model 调用通义千问生成回复
4 HistoryManager 将本轮对话写入记忆库

请求体模型支持前后端 camelCase 对齐:

class ChatRoleIn(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    mode: str = "character"
    role_name: str = Field(default="", alias="roleName")
    thinking_enabled: bool | None = Field(default=None, alias="thinkingEnabled")
    history: list[ChatMessage] = Field(default_factory=list)

核心路由逻辑(节选):

@app.post("/v1/chat/role")
def chat_role(body: ChatRoleIn):
    # Step 1: 记忆增强
    if prompt_enhancer and body.role_name.strip():
        enhanced_context, retrieved_memories = prompt_enhancer.enhance_prompt(...)

    # Step 2~3: Agent 优先,否则直连 LLM
    agent = _build_role_agent(body.role_name, body.thinking_enabled)
    if agent is not None:
        result = agent.respond(message=message, context=enhanced_context, ...)
    else:
        result = _call_chat_model(mode=mode, context=enhanced_context, ...)

    # Step 4: 持久化
    if history_manager and result.get("reply"):
        history_manager.save_conversation(role_id=body.role_name, ...)
    return result

流式与非流式的差异

维度 /v1/chat/role /v1/chat/stream
传输 一次性 JSON SSE 逐 token
thinking 参数 thinking.type enable_thinking
超时 默认 30s 默认 120s
适用 短回复、接口测试 前端打字机效果

四、Java 桥接层与前端调用

4.1 同步转发:AiBridgeService

Java 不解析 AI 业务逻辑,只做 HTTP 透传 + 异常兜底

public Map<String, Object> chatRole(Map<String, Object> body) {
    try {
        String url = appProperties.getAiService().getBaseUrl() + "/v1/chat/role";
        return postJson(url, body);
    } catch (RestClientException e) {
        return Map.of(
            "reply", "(AI 中台未启动)请运行 ai-service 并配置大模型 API 后使用角色对话。",
            "offline", true
        );
    }
}

最佳实践:所有 AI 调用都返回 offline 字段,前端据此展示友好提示,而不是空白或报错弹窗。

4.2 流式转发:AiStreamingService

SSE 链路最容易在 中间层缓冲 处断掉。Java 侧必须:

  1. 设置 Content-Type: text/event-stream
  2. 设置 X-Accel-Buffering: no(穿透 Nginx)
  3. 读超时 ≥ 100s(thinking 模式耗时长)
  4. 逐行转发,不做 JSON 重组
response.setContentType("text/event-stream;charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
response.setHeader("X-Accel-Buffering", "no");
connection.setReadTimeout(100000);

while ((line = reader.readLine()) != null) {
    outputStream.write((line + "\n").getBytes(StandardCharsets.UTF_8));
    outputStream.flush();  // 关键:每行立即 flush
}

Controller 入口:

@RestController
@RequestMapping("/api/ai")
public class AiFeatureController {
    @PostMapping("/chat/role")
    public ApiResult<Map<String, Object>> chatRole(@RequestBody Map<String, Object> body) {
        return ApiResult.ok(aiBridgeService.chatRole(body));
    }

    @PostMapping("/chat/stream")
    public void streamChat(@RequestBody Map<String, Object> body, HttpServletResponse response) {
        aiStreamingService.streamChat(body, response);
    }
}

4.3 前端 SSE 消费示例

AiChat.vue 使用原生 fetch + ReadableStream(而非 axios),因为 axios 对 SSE 支持较弱:

const requestBody = {
  mode: mode.value,
  roleName: roleName.value,
  context: mergedContext,
  message: text,
  thinkingEnabled: thinkingEnabled.value,
  history,
}

const response = await fetch('/api/ai/chat/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${localStorage.getItem('vistaread_token') || ''}`,
  },
  body: JSON.stringify(requestBody),
})

const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
// 逐行解析 "data: {...}",区分 content / reasoning / done

五、API 接口清单

Python AI 中台(:8000

路径 方法 功能
/health GET 服务与子模块就绪状态
/v1/chat/role POST 角色/作者同步对话
/v1/chat/stream POST 角色/作者流式对话(SSE)
/v1/plot/advance POST 剧情推演单步推进
/v1/plot/advance/stream POST 剧情推演流式推进
/v1/plot/branch/goto POST 分支树回溯
/v1/extract/world POST 世界观信息提取
/v1/extract/role POST 角色信息提取
/v1/knowledge/outline POST 知识图谱占位
/v1/memory/* 记忆增删改查(独立 router)

Java 网关(:8080,前端通过 /api 访问)

路径 转发至 Python 模式
POST /api/ai/chat/role /v1/chat/role 同步
POST /api/ai/chat/stream /v1/chat/stream SSE
POST /api/plot/{bookId}/advance /v1/plot/advance 同步
POST /api/plot/{bookId}/advance/stream /v1/plot/advance/stream SSE
POST /api/plot/{bookId}/branch/goto /v1/plot/branch/goto 同步

六、踩坑记录与 FAQ

Q1:.env 已配置 Key,但 /health 显示 memory_ready: false

原因DASHSCOPE_API_KEY 仍为占位符 your-api-key-here,或 uvicorn 工作目录不在 ai-service 导致 .env 未加载。

解决:确认 load_dotenv(dotenv_path=Path(__file__).resolve().parent / ".env") 生效;重启服务后再次 curl /health

Q2:同步对话正常,流式无输出或一次性返回

原因:Java / Nginx 缓冲了 SSE;或 Python 侧 enable_thinking 与 API 版本不匹配。

解决:检查 X-Accel-Buffering: no;Java 侧每行 flush();浏览器 Network 面板确认 Content-Typetext/event-stream

Q3:角色对话没有「人设感」,像通用助手

原因role_profiles.json 中无该角色 profile,走了 _call_chat_model 降级路径。

解决:检查 _find_role_profile() 的别名匹配;或通过 /v1/extract/role 先提取角色资料。

Q4:Java 返回「AI 中台未启动」但 Python 明明在跑

原因application.ymlbase-url 端口不对,或防火墙拦截。

解决:确认 vistaread.ai-service.base-url: http://localhost:8000;Java 日志中搜索 [AI Stream] 目标URL

Q5:thinking 模式开启后超时

原因:推理链较长,默认 30s 超时不够。

解决.env 设置 LLM_TIMEOUT=120;Java AiStreamingService 读超时已设为 100s。


七、经验总结与后续计划

7.1 三条最佳实践

  1. 配置与代码分离:Agent 行为由 .env + JSON profile 驱动,代码只负责编排,便于实训演示时快速切换模型。
  2. 失败可降级:记忆模块、Agent profile、AI 中台任一环节失败,都不应阻断基础对话——每层返回 offline 或友好文案。
  3. 流式链路端到端 flush:Python StreamingResponse → Java 逐行写 → 浏览器 ReadableStream,任何一层缓冲都会导致「假流式」。

7.2 Agent 配置三层模型(小结)

层次 载体 作用
环境层 .env API 地址、密钥、模型名、超时
客户端层 LlmClient / _call_chat_model OpenAI 兼容 HTTP 封装
Agent 层 RoleAgent + WorldAgent + PlotOrchestrator 人设、编排、多智能体协作

这一底座已支撑角色对话、剧情推演、世界/角色提取、记忆检索等全部 AI 能力。后续新业务只需扩展 /v1/* 协议,无需重复搭建调用链路。

7.3 后续计划

  • 补充 ai-service/.env.example 模板
  • Java AiProvider 与 Python 配置对齐,支持备用通道
  • 角色 profile 热加载,免重启
  • 统一 AI 调用监控(耗时、失败率)
  • docker-compose 纳入 ai-service 容器

如果本文对你搭建「Java + Python 双后端 + 大模型 Agent」架构有帮助,欢迎收藏。下一篇工作记录将深入 多智能体剧情推演的 PlotOrchestrator 编排细节

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐