【项目实训个人工作记录(六)】—— Agent的初步配置
项目:阅见(VistaRead)—— 基于大模型的深度交互式阅读平台
技术栈:Vue 3 · Spring Boot · FastAPI · 通义千问 · ChromaDB · SSE
难度:中级 | 阅读时长:约 18 分钟
关键词:大模型 Agent、OpenAI 兼容 API、FastAPI 中台、多智能体、流式对话 SSE
日期: 2026年4月20日——2026年5月10日
项目: 阅见:基于大模型 的交互式小说阅读平台
前言:为什么需要「AI 中台」而不是前端直调大模型?
在实训初期,团队曾讨论过「Vue 页面直接调用通义千问 API」的方案。该方案开发快,但很快暴露出三类问题:
- 密钥泄露风险:API Key 无法安全放在浏览器端;
- 业务与 AI 耦合:角色记忆、剧情状态、用户鉴权无法统一编排;
- 难以扩展 Agent:WorldAgent、RoleAgent 等多智能体逻辑不适合堆在前端或 Java 业务层。
最终,「阅见」采用 Vue → Java 网关 → Python AI 中台 → 大模型 API 的四段式链路。Java 负责鉴权与持久化,Python 负责 Agent 编排与向量检索,前端只关心 /api/* 业务接口。
本文你将学到:
- 三层 AI 架构的设计动机与职责边界
ai-service/.env环境变量如何驱动 Agent 启动LlmClient、RoleAgent、WorldAgent的配置与协作方式- Java
AiBridgeService/AiStreamingService如何实现同步与 SSE 流式转发 - 本地联调步骤、健康检查方法与 5 个常见踩坑 FAQ
目录
一、整体架构与设计原则
1.1 四层调用链路
以一次 角色流式对话 为例,请求依次经过四层:
静态部署关系如下:
┌─────────────┐ /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.vue、PlotBranch.vue |
组装请求体、消费 SSE 流 |
| Java | AiFeatureController、PlotController |
鉴权、转发、离线降级 |
| Java | AiBridgeService、AiStreamingService |
同步 REST / SSE 桥接 |
| Python | main.py |
路由注册、lifespan 初始化、对话入口 |
| Python | llm_client.py |
OpenAI 兼容 HTTP 封装 |
| Python | role_agent.py、world_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
核心依赖:fastapi、uvicorn、httpx、python-dotenv、dashscope、chromadb。
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
提示:记忆模块初始化失败时,服务仍会启动,但
/health中memory_ready为false,对话功能不受影响,只是无法检索历史记忆。
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": false 且 reply 有内容,说明 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() 的三处关键逻辑:
- 自动补全 URL 为
.../chat/completions - 支持
thinking: {type: enabled/disabled}思考模式 - 失败时最多重试 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:
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 侧必须:
- 设置
Content-Type: text/event-stream - 设置
X-Accel-Buffering: no(穿透 Nginx) - 读超时 ≥ 100s(thinking 模式耗时长)
- 逐行转发,不做 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-Type 为 text/event-stream。
Q3:角色对话没有「人设感」,像通用助手
原因:role_profiles.json 中无该角色 profile,走了 _call_chat_model 降级路径。
解决:检查 _find_role_profile() 的别名匹配;或通过 /v1/extract/role 先提取角色资料。
Q4:Java 返回「AI 中台未启动」但 Python 明明在跑
原因:application.yml 中 base-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 三条最佳实践
- 配置与代码分离:Agent 行为由
.env+ JSON profile 驱动,代码只负责编排,便于实训演示时快速切换模型。 - 失败可降级:记忆模块、Agent profile、AI 中台任一环节失败,都不应阻断基础对话——每层返回
offline或友好文案。 - 流式链路端到端 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 编排细节。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)