update:1.优化世界观生成提示词 2.修复章节分析页面内容重复问题 3.限制mcp调用最大并发数

This commit is contained in:
xiamuceer
2025-11-24 20:42:09 +08:00
parent 187d62f315
commit 4354e74fff
7 changed files with 251 additions and 274 deletions
+6 -6
View File
@@ -95,15 +95,15 @@ async def world_building_generator(
3. 相关领域的专业知识
4. 类似作品的设定参考
根据题材特点,有针对性地查询2-3个关键问题"""
查询最关键的1个问题(不要超过1个)"""
# 调用MCP增强的AI(非流式,最多2轮工具调用)
# 调用MCP增强的AI(非流式,最多1轮工具调用,避免超时
planning_result = await user_ai_service.generate_text_with_mcp(
prompt=planning_prompt,
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=2,
max_tool_rounds=1,
tool_choice="auto",
provider=None,
model=None
@@ -365,15 +365,15 @@ async def characters_generator(
3. 职业特点和生活方式
4. 相关领域的人物原型
根据题材特点,有针对性地查询1-2个关键问题"""
查询最关键的1个问题(不要超过1个)"""
# 调用MCP增强的AI(非流式,最多2轮工具调用)
# 调用MCP增强的AI(非流式,最多1轮工具调用,避免超时
planning_result = await user_ai_service.generate_text_with_mcp(
prompt=planning_prompt,
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=2,
max_tool_rounds=1, # ✅ 优化: 从2轮减少到1轮
tool_choice="auto",
provider=None,
model=None
+5 -5
View File
@@ -73,10 +73,10 @@ def _get_or_create_http_client(
client = httpx.AsyncClient(
timeout=httpx.Timeout(
connect=60.0, # 连接超时
read=180.0, # 读取超时
write=60.0, # 写入超时
pool=60.0 # 连接池超时
connect=90.0, # 连接超时
read=300.0, # 读取超时
write=90.0, # 写入超时
pool=90.0 # 连接池超时
),
limits=limits,
headers={
@@ -959,7 +959,7 @@ class AIService:
else:
# 达到最大轮次
logger.warning(f"达到MCP最大调用轮次 {max_tool_rounds}")
logger.info(f"达到MCP最大调用轮次 {max_tool_rounds}")
result["content"] = conversation_history[-1].get("content", "")
result["finish_reason"] = "max_rounds"
+47 -32
View File
@@ -291,16 +291,18 @@ class MCPToolService:
user_id: str,
tool_calls: List[Dict[str, Any]],
db_session: AsyncSession,
timeout: Optional[float] = None
timeout: Optional[float] = None,
max_concurrent: int = 2
) -> List[Dict[str, Any]]:
"""
批量执行AI请求的工具调用(并行执行
批量执行AI请求的工具调用(限制并发数,避免超时
Args:
user_id: 用户ID
tool_calls: AI返回的工具调用列表
db_session: 数据库会话
timeout: 单个工具调用的超时时间(秒,默认使用配置)
max_concurrent: 最大并发工具调用数(默认2)
Returns:
工具调用结果列表
@@ -311,41 +313,54 @@ class MCPToolService:
# 使用配置的默认超时
actual_timeout = timeout or mcp_config.TOOL_CALL_TIMEOUT_SECONDS
logger.info(f"开始执行 {len(tool_calls)} 个工具调用 (超时={actual_timeout}s)")
logger.info(f"开始执行 {len(tool_calls)} 个工具调用 (超时={actual_timeout}s, 最大并发={max_concurrent})")
# 创建异步任务列表
tasks = [
self._execute_single_tool(
user_id=user_id,
tool_call=tool_call,
db_session=db_session,
timeout=actual_timeout
)
for tool_call in tool_calls
]
# ✅ 分批执行,每批最多max_concurrent个
all_results = []
for i in range(0, len(tool_calls), max_concurrent):
batch = tool_calls[i:i+max_concurrent]
batch_num = i // max_concurrent + 1
total_batches = (len(tool_calls) + max_concurrent - 1) // max_concurrent
# 并行执行所有工具调用
results = await asyncio.gather(*tasks, return_exceptions=True)
logger.info(f"执行工具批次 {batch_num}/{total_batches}, 数量: {len(batch)}")
# 处理结果
formatted_results = []
for i, result in enumerate(results):
tool_call = tool_calls[i]
# 创建当前批次的异步任务
tasks = [
self._execute_single_tool(
user_id=user_id,
tool_call=tool_call,
db_session=db_session,
timeout=actual_timeout
)
for tool_call in batch
]
if isinstance(result, Exception):
# 工具调用异常
formatted_results.append({
"tool_call_id": tool_call.get("id", f"call_{i}"),
"role": "tool",
"name": tool_call["function"]["name"],
"content": f"工具调用失败: {str(result)}",
"success": False,
"error": str(result)
})
else:
formatted_results.append(result)
# 并行执行当前批次
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
return formatted_results
# 处理批次结果
for j, result in enumerate(batch_results):
tool_call = batch[j]
if isinstance(result, Exception):
# 工具调用异常
all_results.append({
"tool_call_id": tool_call.get("id", f"call_{i+j}"),
"role": "tool",
"name": tool_call["function"]["name"],
"content": f"工具调用失败: {str(result)}",
"success": False,
"error": str(result)
})
else:
all_results.append(result)
# 批次间增加短暂延迟,避免API限流
if i + max_concurrent < len(tool_calls):
await asyncio.sleep(0.5)
logger.debug(f"批次间延迟 0.5 秒...")
return all_results
async def _execute_single_tool(
self,
+36 -24
View File
@@ -119,27 +119,38 @@ class PromptService:
主题:{theme}
类型:{genre}
# 2. 世界构建框架
请生成包含以下四个核心板块的世界构建框架。请确保所有板块都围绕【核心概念】展开,并且板块之间【互为因果】
# 2. 核心指令(CRITICAL
* **去标签化**:严禁使用通用的“XX纪元”、“XX时代”、“XX年”作为时间背景的开头或核心描述。请直接描述世界所处的状态、技术水平或生存现状
* **动态演绎**:所有设定必须直接由输入的主题衍生而来。例如,如果是赛博朋克,不要只写“高科技”,要写“义体技术如何导致了贫民窟的特定生活方式”。
* **拒绝陈词滥调**:避免使用宏大的空洞词汇,专注于具体的、可感知的细节。
1. **时间背景 (time_period)**
* 具体的时代设定(例如:星际航行晚期、黑铁时代)
* 重要的【历史转折事件】(是什么导致了当前的世界面貌?)。
* 当前的主要【社会矛盾】或【时代议题】。
2. **地理/空间 (location)**
* 主要舞台(如城市、星球、位面)的【地理环境特征】。
* 这些特征如何影响了【文明】的形态和【资源】分布?
* 独特的【空间布局】或【奇观】。
3. **氛围基调 (atmosphere)**
* 整体的【情感色彩】(例如:压抑、荒诞、史诗、诡异)。
* 【视觉风格】(例如:赛博霓虹、蒸汽朋克、哥特式)。
* 普通居民在日常生活中最常【感受】到什么?
4. **世界规则 (rules)**
* 【物理法则】或【超自然力量】(如魔法、科技)的【具体运作方式】和【代价】。
* 【社会规则】和【权力结构】(谁在统治?基于什么?)。
* 最严重的【社会禁忌】是什么?违反了会怎样?
# 3. 世界构建框架
请生成包含以下四个核心板块的JSON。请确保所有板块互为因果,逻辑严密
# 3. 严格格式要求
**重要说明:每个字段的value必须是一个完整的文本字符串,将以下所有要点整合成连贯的段落描述,不要使用嵌套的JSON对象或数组。**
1. **time_period (时间线与文明阶段)**
请将以下内容整合为一段完整的文字描述(300-500字):
* 描述当前世界处于什么**发展阶段**(是毁灭边缘、新生萌芽、还是停滞不前?),**不要给这个阶段起名字**,而是描述其**特征**。
* **历史转折点**:具体的事件(战争、发明、灾难),它如何直接导致了现在的局面?
* **当下的核心矛盾**:时间流逝带来的具体焦虑是什么?(例如:资源枯竭的倒计时、某种信仰的崩塌)。
2. **location (空间与生态环境)**
请将以下内容整合为一段完整的文字描述(300-500字):
* **舞台特征**:描述主要故事发生的地理或空间环境(如:悬浮的破碎岛屿、被真菌覆盖的地铁网络)。
* **环境与生存**:地理环境如何强迫居民改变了生活方式?(例如:因为引力失衡,建筑都是倒挂的)。
* **标志性奇观**:一个能代表这个世界独特性的具体场景或建筑。
3. **atmosphere (感官体验与基调)**
请将以下内容整合为一段完整的文字描述(300-500字):
* **感官细节**:如果站在这个世界的街头,会**闻**到什么?**听**到什么?(不要只写"压抑",要写"空气中弥漫着铁锈和合成营养膏的酸味")。
* **视觉美学**:描述具体的色彩倾向和光影质感。
* **居民心态**:普通人普遍的心理状态(是麻木、狂热、还是某种特定的恐惧)。
4. **rules (运作逻辑与禁忌)**
请将以下内容整合为一段完整的文字描述(300-500字):
* **核心法则**:这个世界运行的底层逻辑(物理、魔法或科技)。**重点描述代价**(使用力量需要支付什么?)。
* **权力架构**:谁掌握资源?他们通过什么手段维持控制(暴力、技术垄断、宗教洗脑)?
* **红线禁忌**:这个社会绝对不能触碰的具体底线,以及违反后的直接后果。
# 4. 严格格式要求
1. **绝对纯净JSON**:你的[唯一]输出必须是一个完整的JSON对象。输出必须以左花括号开始,并以右花括号结束。
2. **禁止额外字符**:不要在JSON对象之前或之后包含任何说明文字、Markdown标记(如三个反引号加json)、注释或任何其他非JSON字符。
3. **JSON内部文本规则**:在JSON的value字符串内部:
@@ -147,13 +158,14 @@ class PromptService:
* 所有【专有名词】(如地点、人物、组织)应使用【】包裹。
* 所有《作品》或《特殊概念》的标题应使用《》包裹。
4. **JSON结构**:严格遵守`"key": "value"`的英文双引号结构,并使用下面指定的key。
5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节。
5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节,整合为连贯的段落文本
6. **禁止嵌套结构**:value必须是纯文本字符串,绝对不能是JSON对象或数组,所有信息都要整合在一个字符串中。
{{
"time_period": "(此处填写时间背景的详细描述",
"location": "(此处填写地理/空间的详细描述",
"atmosphere": "(此处填写氛围基调的详细描述",
"rules": "(此处填写世界规则的详细描述"
"time_period": "(此处填写一段完整的文字描述,包含发展阶段特征、历史转折点、核心矛盾等内容,300-500字",
"location": "(此处填写一段完整的文字描述,包含舞台特征、环境与生存、标志性奇观等内容,300-500字",
"atmosphere": "(此处填写一段完整的文字描述,包含感官细节、视觉美学、居民心态等内容,300-500字",
"rules": "(此处填写一段完整的文字描述,包含核心法则、权力架构、红线禁忌等内容,300-500字"
}}"""
# 批量角色生成提示词
-156
View File
@@ -1,156 +0,0 @@
# 安装和测试指南
## 1. 安装前端依赖
在完成所有代码更改后,需要安装新添加的npm包:
```bash
cd frontend
npm install
```
这将安装以下新依赖:
- `react-diff-viewer-continued`: 用于版本对比的diff查看器
## 2. 重启后端服务
由于修改了数据模型,需要重启后端服务以加载新的模型定义:
```bash
# 在项目根目录
cd backend
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
## 3. 启动前端开发服务器
```bash
cd frontend
npm run dev
```
## 4. 测试功能流程
### 4.1 基本流程测试
1. **打开章节列表**
- 进入任意项目
- 查看章节列表
2. **分析章节**
- 点击某个章节的"分析"按钮
- 等待AI分析完成
- 查看分析结果和改进建议
3. **重新生成章节**
- 在分析结果页面,点击"根据建议重新生成"
- 选择要应用的建议
- 可以添加自定义修改要求
- 配置生成参数(字数、保留元素等)
- 勾选"保存为版本历史"(不勾选自动应用)
- 点击"开始重新生成"
- 观察流式生成过程
4. **查看版本对比**
- 生成完成后,点击"查看版本对比"按钮
- 进入版本管理器界面
5. **版本管理操作**
- **版本列表**: 查看所有历史版本
- **预览版本**: 点击"预览"查看某个版本的完整内容
- **对比版本**:
- 点击第一个版本的"对比"按钮
- 再点击第二个版本的"对比"按钮
- 自动切换到"对比"标签页
- 查看并排diff对比
- **恢复版本**: 点击"恢复"将章节内容还原到该版本
- **删除版本**: 删除不需要的历史版本(当前激活版本不能删除)
### 4.2 测试场景
#### 场景1:优化情感描写
1. 分析一个章节
2. 查看建议中关于情感的建议
3. 重新生成时选择情感相关建议
4. 设置重点优化方向为"情感渲染"
5. 生成后对比新旧版本的差异
#### 场景2:调整节奏
1. 选择节奏问题的建议
2. 添加自定义指令:"加快前半部分节奏,增强紧张感"
3. 设置目标字数适当减少(如从3000减到2500)
4. 生成后查看结构变化
#### 场景3:版本管理
1. 对同一章节重新生成多次(使用不同建议)
2. 在版本管理器中浏览所有版本
3. 对比不同版本的差异
4. 选择最满意的版本恢复
## 5. 验证清单
- [ ] 依赖安装无错误
- [ ] 前后端服务正常启动
- [ ] 章节分析功能正常
- [ ] 重新生成功能正常
- [ ] 流式生成显示正常
- [ ] 版本保存成功
- [ ] 版本列表显示正确
- [ ] 版本预览功能正常
- [ ] 版本对比diff显示正确
- [ ] 版本恢复功能正常
- [ ] 版本删除功能正常
- [ ] 移动端适配正常
## 6. 常见问题
### Q1: 依赖安装失败
```bash
# 清除缓存重试
npm cache clean --force
npm install
```
### Q2: 后端启动报错
- 检查是否运行了数据库迁移脚本
- 确认模型定义与数据库表结构一致
### Q3: 版本对比不显示
- 检查浏览器控制台是否有JavaScript错误
- 确认react-diff-viewer-continued正确安装
### Q4: 重新生成后看不到新内容
- 检查是否勾选了"自动应用"
- 查看版本管理器中是否有新版本记录
## 7. 性能优化建议
1. **首次加载优化**
- react-diff-viewer-continued是较大的依赖
- 可以考虑代码分割(lazy loading
2. **版本列表优化**
- 如果版本过多,考虑分页加载
- 添加版本数量限制提示
3. **diff计算优化**
- 对于超长文本,可以限制diff行数
- 添加加载提示
## 8. 下一步优化方向
1. **AI质量评分对比**
- 在版本对比时显示质量分数变化
- 自动标注改进/退步的指标
2. **批量操作**
- 支持批量删除历史版本
- 支持版本导出/导入
3. **协作功能**
- 版本评论和讨论
- 多人协作编辑
4. **智能推荐**
- 基于历史生成结果推荐最佳配置
- 学习用户偏好自动调整参数
+133 -42
View File
@@ -24,6 +24,7 @@ interface TextSegment {
type: 'text' | 'annotated';
content: string;
annotation?: MemoryAnnotation;
annotations?: MemoryAnnotation[]; // 🔧 支持多个标注
}
interface AnnotatedTextProps {
@@ -108,30 +109,89 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
const result: TextSegment[] = [];
let lastPos = 0;
// 🔧 智能分组:检测重叠和相邻的标注
const annotationRanges: Array<{
start: number;
end: number;
annotations: MemoryAnnotation[];
}> = [];
for (const annotation of processedAnnotations) {
const { position, length } = annotation;
const actualLength = length > 0 ? length : 30;
const start = position;
const end = position + actualLength;
// 添加普通文本片段
if (position > lastPos) {
// 查找是否有重叠或紧邻的范围
const overlappingRange = annotationRanges.find(
(range) =>
(start >= range.start && start <= range.end) || // 起始点在范围内
(end >= range.start && end <= range.end) || // 结束点在范围内
(start <= range.start && end >= range.end) || // 完全包含
Math.abs(start - range.end) <= 5 || // 紧邻(容差5字符)
Math.abs(end - range.start) <= 5
);
if (overlappingRange) {
// 合并到现有范围
overlappingRange.start = Math.min(overlappingRange.start, start);
overlappingRange.end = Math.max(overlappingRange.end, end);
overlappingRange.annotations.push(annotation);
} else {
// 创建新范围
annotationRanges.push({
start,
end,
annotations: [annotation],
});
}
}
// 按起始位置排序
annotationRanges.sort((a, b) => a.start - b.start);
// 🔧 智能分片:将重叠区域分成多个小片段
for (const range of annotationRanges) {
// 添加前面的普通文本
if (range.start > lastPos) {
result.push({
type: 'text',
content: content.slice(lastPos, position),
content: content.slice(lastPos, range.start),
});
}
// 添加标注片段
const annotatedContent = content.slice(
position,
position + (length > 0 ? length : 30) // 如果没有长度,默认30字符
);
if (range.annotations.length === 1) {
// 单个标注,直接添加
result.push({
type: 'annotated',
content: content.slice(range.start, range.end),
annotation: range.annotations[0],
annotations: range.annotations,
});
} else {
// 🔧 多个标注:将文本分成多个小片段
const totalLength = range.end - range.start;
const segmentLength = Math.max(1, Math.floor(totalLength / range.annotations.length));
result.push({
type: 'annotated',
content: annotatedContent,
annotation,
});
// 按重要性排序标注
const sortedAnnotations = [...range.annotations].sort((a, b) => b.importance - a.importance);
lastPos = position + (length > 0 ? length : 30);
for (let i = 0; i < sortedAnnotations.length; i++) {
const segmentStart = range.start + i * segmentLength;
const segmentEnd = i === sortedAnnotations.length - 1
? range.end
: range.start + (i + 1) * segmentLength;
result.push({
type: 'annotated',
content: content.slice(segmentStart, segmentEnd),
annotation: sortedAnnotations[i],
annotations: sortedAnnotations, // 保留所有标注信息
});
}
}
lastPos = range.end;
}
// 添加剩余文本
@@ -142,6 +202,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
});
}
console.log(`AnnotatedText: 处理${processedAnnotations.length}个标注,生成${result.length}个片段`);
return result;
}, [content, processedAnnotations]);
@@ -151,43 +212,73 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
return <span key={index}>{segment.content}</span>;
}
const { annotation } = segment;
const { annotation, annotations } = segment;
if (!annotation) return null;
const color = TYPE_COLORS[annotation.type];
const icon = TYPE_ICONS[annotation.type];
const isActive = activeAnnotationId === annotation.id;
// 工具提示内容
// 🔧 工具提示内容:如果有多个标注,显示所有标注信息
const tooltipContent = (
<div style={{ maxWidth: 300 }}>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
{icon} {annotation.title}
</div>
<div style={{ fontSize: 12, opacity: 0.9 }}>
{annotation.content.slice(0, 100)}
{annotation.content.length > 100 ? '...' : ''}
</div>
<div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
: {(annotation.importance * 10).toFixed(1)}/10
</div>
{annotation.tags && annotation.tags.length > 0 && (
<div style={{ marginTop: 4, fontSize: 11 }}>
{annotation.tags.map((tag, i) => (
<span
key={i}
style={{
display: 'inline-block',
background: 'rgba(255,255,255,0.2)',
padding: '2px 6px',
borderRadius: 3,
marginRight: 4,
}}
>
{tag}
</span>
<div style={{ maxWidth: 350 }}>
{annotations && annotations.length > 1 ? (
// 多个标注
<div>
<div style={{ fontWeight: 'bold', marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.3)', paddingBottom: 4 }}>
📍 {annotations.length}
</div>
{annotations.map((ann, idx) => (
<div key={ann.id} style={{
marginBottom: idx < annotations.length - 1 ? 8 : 0,
paddingBottom: idx < annotations.length - 1 ? 8 : 0,
borderBottom: idx < annotations.length - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none'
}}>
<div style={{ fontWeight: 'bold', marginBottom: 4, fontSize: 13 }}>
{TYPE_ICONS[ann.type]} {ann.title}
</div>
<div style={{ fontSize: 11, opacity: 0.9 }}>
{ann.content.slice(0, 80)}
{ann.content.length > 80 ? '...' : ''}
</div>
<div style={{ marginTop: 4, fontSize: 10, opacity: 0.7 }}>
: {(ann.importance * 10).toFixed(1)}/10
</div>
</div>
))}
</div>
) : (
// 单个标注
<div>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
{icon} {annotation.title}
</div>
<div style={{ fontSize: 12, opacity: 0.9 }}>
{annotation.content.slice(0, 100)}
{annotation.content.length > 100 ? '...' : ''}
</div>
<div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
: {(annotation.importance * 10).toFixed(1)}/10
</div>
{annotation.tags && annotation.tags.length > 0 && (
<div style={{ marginTop: 4, fontSize: 11 }}>
{annotation.tags.map((tag, i) => (
<span
key={i}
style={{
display: 'inline-block',
background: 'rgba(255,255,255,0.2)',
padding: '2px 6px',
borderRadius: 3,
marginRight: 4,
}}
>
{tag}
</span>
))}
</div>
)}
</div>
)}
</div>
);
+20 -5
View File
@@ -57,12 +57,9 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
};
}, [visible, chapterId]);
const fetchAnalysisStatus = async () => {
// 🔧 新增:独立的章节信息加载函数
const loadChapterInfo = async () => {
try {
setLoading(true);
setError(null);
// 同时获取章节信息
const chapterResponse = await fetch(`/api/chapters/${chapterId}`);
if (chapterResponse.ok) {
const chapterData = await chapterResponse.json();
@@ -71,7 +68,20 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
chapter_number: chapterData.chapter_number,
content: chapterData.content || ''
});
console.log('✅ 已刷新章节内容,字数:', chapterData.content?.length || 0);
}
} catch (error) {
console.error('❌ 加载章节信息失败:', error);
}
};
const fetchAnalysisStatus = async () => {
try {
setLoading(true);
setError(null);
// 🔧 使用独立的章节加载函数
await loadChapterInfo();
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
@@ -134,6 +144,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
if (taskData.status === 'completed') {
clearInterval(pollInterval);
await fetchAnalysisResult();
// 🔧 分析完成后刷新章节内容,确保显示最新内容
await loadChapterInfo();
} else if (taskData.status === 'failed') {
clearInterval(pollInterval);
setError(taskData.error_message || '分析失败');
@@ -152,6 +164,9 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
setLoading(true);
setError(null);
// 🔧 触发分析前先刷新章节内容,确保分析的是最新内容
await loadChapterInfo();
const response = await fetch(`/api/chapters/${chapterId}/analyze`, {
method: 'POST'
});