199
README.md
199
README.md
@@ -1,117 +1,114 @@
|
||||
<img src="depends-data/maimai.png" alt="MaiBot" title="作者:略nd" width="300">
|
||||
|
||||
# 麦麦!MaiCore-MaiBot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://deepwiki.com/DrSmoothl/MaiBot)
|
||||
|
||||
<div style="text-align: center">
|
||||
<strong>
|
||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P">🌟 演示视频</a> |
|
||||
<a href="#-更新和安装">🚀 快速入门</a> |
|
||||
<a href="#-文档">📃 教程</a> |
|
||||
<a href="#-讨论">💬 讨论</a> |
|
||||
<a href="#-贡献和致谢">🙋 贡献指南</a>
|
||||
</strong>
|
||||
<div align="center">
|
||||
<h1>麦麦 MaiBot <sub><small>MaiCore</small></sub></h1>
|
||||
|
||||
<!-- Badges Row -->
|
||||
<p>
|
||||
<img src="https://img.shields.io/badge/Python-3.10+-blue" alt="Python Version">
|
||||
<img src="https://img.shields.io/github/license/Mai-with-u/MaiBot?label=%E5%8D%8F%E8%AE%AE" alt="License">
|
||||
<img src="https://img.shields.io/badge/状态-开发中-yellow" alt="Status">
|
||||
<img src="https://img.shields.io/github/contributors/Mai-with-u/MaiBot.svg?style=flat&label=%E8%B4%A1%E7%8C%AE%E8%80%85" alt="Contributors">
|
||||
<img src="https://img.shields.io/github/forks/Mai-with-u/MaiBot.svg?style=flat&label=%E5%88%86%E6%94%AF%E6%95%B0" alt="Forks">
|
||||
<img src="https://img.shields.io/github/stars/Mai-with-u/MaiBot?style=flat&label=%E6%98%9F%E6%A0%87%E6%95%B0" alt="Stars">
|
||||
<a href="https://deepwiki.com/DrSmoothl/MaiBot"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- Mascot on the Right (Float) -->
|
||||
<img src="depends-data/maimai-v2.png" align="right" width="40%" alt="MaiBot Character" style="margin-left: 20px; margin-bottom: 20px;">
|
||||
|
||||
## 🎉 介绍
|
||||
|
||||
**🍔MaiCore 是一个基于大语言模型的可交互智能体**
|
||||
**🍔 MaiCore 是一个基于大语言模型的可交互智能体**
|
||||
|
||||
- 💭 **拟人构建的prompt**:使用自然语言风格构建回复器的prompt,实现近似人类言语习惯的回复。
|
||||
- 💭 **行为规划**:在合适的时间说话,使用合适的动作
|
||||
- 🧠 **表达学习**:学习群友的说话风格和表达方式,学会真实人类的说话风格
|
||||
- 🤔 **黑话学习**:自主的学习没有见过的词语,尝试理解并认知含义
|
||||
- 🔌 **插件系统**:提供API和事件系统,可编写丰富插件。
|
||||
- 💝 **情感表达**:情绪系统和表情包系统。
|
||||
MaiBot 不仅仅是一个机器人,她致力于成为一个活跃在 QQ 群聊中的“生命体”。她不追求完美,但追求真实。
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
|
||||
- 💭 **拟人构建**:使用自然语言风格构建 Prompt,回复贴近人类习惯。
|
||||
- 🎭 **行为规划**:懂得在合适的时间说话,使用合适的动作。
|
||||
- 🧠 **表达学习**:模仿群友的说话风格,学习黑话,不断进化。
|
||||
- 🔌 **插件系统**:提供强大的 API 和事件系统,无限扩展可能。
|
||||
- 💝 **情感表达**:拥有独立的情绪系统和表情包互动能力。
|
||||
|
||||
### 🚀 快速导航
|
||||
<p>
|
||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P">🌟 演示视频</a> |
|
||||
<a href="#-更新和安装">📦 快速入门</a> |
|
||||
<a href="#-部署教程">📃 核心文档</a> |
|
||||
<a href="#-讨论与社区">💬 加入社区</a>
|
||||
</p>
|
||||
|
||||
<!-- Clear float to ensure subsequent content starts below the image area if text is short -->
|
||||
<br clear="both">
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
<h3>🎥 精彩演示</h3>
|
||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
|
||||
<picture>
|
||||
<source media="(max-width: 600px)" srcset="depends-data/video.png" width="100%">
|
||||
<img src="depends-data/video.png" width="30%" alt="麦麦演示视频">
|
||||
<img src="depends-data/video.png" width="60%" alt="麦麦演示视频" style="border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
|
||||
</picture>
|
||||
<br />
|
||||
👆 点击观看麦麦演示视频 👆
|
||||
</a>
|
||||
<br>
|
||||
<small>👆 点击观看麦麦演示视频 👆</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🔥 更新和安装
|
||||
|
||||
> **最新版本: v0.12.2** ([📄 更新日志](changelogs/changelog.md))
|
||||
|
||||
**最新版本: v0.12.0** ([更新日志](changelogs/changelog.md))
|
||||
- **下载**: 前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
|
||||
- **启动器**: [Mailauncher](https://github.com/MaiM-with-u/mailauncher/releases/) (仅支持 MacOS, 早期开发中)
|
||||
|
||||
| 分支 | 说明 |
|
||||
| :--- | :--- |
|
||||
| `main` | ✅ **稳定发布版本 (推荐)** |
|
||||
| `dev` | 🚧 开发测试版本 (不稳定) |
|
||||
| `classical` | 🛑 经典版本 (停止维护) |
|
||||
|
||||
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
|
||||
|
||||
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器
|
||||
|
||||
注意,启动器处于早期开发版本,仅支持MacOS
|
||||
|
||||
**GitHub 分支说明:**
|
||||
- `main`: 稳定发布版本(推荐)
|
||||
|
||||
- `dev`: 开发测试版本(不稳定)
|
||||
|
||||
- `classical`: 经典版本(停止维护)
|
||||
|
||||
### 最新版本部署教程
|
||||
- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于 MaiCore 的新版本部署方式(与旧版本不兼容)
|
||||
### 📚 部署教程
|
||||
👉 **[🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html)**
|
||||
*(注意:MaiCore 新版本部署方式与旧版本不兼容)*
|
||||
|
||||
> [!WARNING]
|
||||
> - 项目处于活跃开发阶段,功能和 API 可能随时调整。
|
||||
> - 有问题可以提交 Issue 。
|
||||
> - QQ 机器人存在被限制风险,请自行了解,谨慎使用。
|
||||
> - 由于程序处于开发中,可能消耗较多 token。
|
||||
> - ⚠️ 项目处于活跃开发阶段,API 可能随时调整。
|
||||
> - ⚠️ QQ 机器人存在风控风险,请谨慎使用。
|
||||
> - ⚠️ AI 模型运行可能消耗较多 Token。
|
||||
|
||||
## 💬 讨论
|
||||
---
|
||||
|
||||
**技术交流群/答疑群:**
|
||||
[麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW) |
|
||||
[麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs) |
|
||||
[麦麦要当VTB](https://qm.qq.com/q/wGePTl1UyY) |
|
||||
## 💬 讨论与社区
|
||||
|
||||
为了维持技术交流和互帮互助的氛围,请不要在技术交流群讨论过多无关内容~
|
||||
我们欢迎所有对 MaiBot 感兴趣的朋友加入!
|
||||
|
||||
**聊天吹水群:**
|
||||
- [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec)
|
||||
| 类别 | 群组 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **技术交流** | [麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW) | 技术交流/答疑 |
|
||||
| **技术交流** | [麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs) | 技术交流/答疑 |
|
||||
| **技术交流** | [麦麦要当VTB](https://qm.qq.com/q/wGePTl1UyY) | 技术交流/答疑 |
|
||||
| **闲聊吹水** | [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec) | 仅限闲聊,不答疑 |
|
||||
| **插件开发** | [插件开发群](https://qm.qq.com/q/1036092828) | 进阶开发与测试 |
|
||||
|
||||
麦麦相关闲聊群,此群仅用于聊天,提问部署/技术问题可能不会快速得到答案
|
||||
|
||||
**插件开发/测试版讨论群:**
|
||||
- [插件开发群](https://qm.qq.com/q/1036092828)
|
||||
|
||||
进阶内容,包括插件开发,测试版使用等等
|
||||
---
|
||||
|
||||
## 📚 文档
|
||||
|
||||
**部分内容可能更新不够及时,请注意版本对应**
|
||||
> [!NOTE]
|
||||
> 部分内容可能更新不够及时,请注意版本对应。
|
||||
|
||||
- [📚 核心 Wiki 文档](https://docs.mai-mai.org) - 项目最全面的文档中心,你可以了解麦麦有关的一切。
|
||||
- **[📚 核心 Wiki 文档](https://docs.mai-mai.org)**: 最全面的文档中心,了解麦麦的一切。
|
||||
|
||||
### 🧩 衍生项目
|
||||
|
||||
## 📚 衍生项目
|
||||
- **[MaiCraft](https://github.com/MaiM-with-u/Maicraft)**: 让麦麦陪你玩 Minecraft (早期开发中)。
|
||||
- **[MoFox_Bot](https://github.com/MoFox-Studio/MoFox-Core)**: 基于 MaiCore 0.10.0 的增强型 Fork,更稳定更有趣。
|
||||
|
||||
### MaiCraft(早期开发)
|
||||
[MaiCraft](https://github.com/MaiM-with-u/Maicraft)
|
||||
> 让麦麦具有玩MC能力的项目
|
||||
> 交流群:1058573197
|
||||
---
|
||||
|
||||
### MoFox_Bot
|
||||
[MoFox - 仓库地址](https://github.com/MoFox-Studio/MoFox-Core)
|
||||
> MoFox_Bot 是一个基于 MaiCore 0.10.0 snapshot.5 的增强型 fork 项目
|
||||
> 我们保留了原项目几乎所有核心功能,并在此基础上进行了深度优化与功能扩展,致力于打造一个更稳定、更智能、更具趣味性的 AI 智能体。
|
||||
|
||||
|
||||
|
||||
## 设计理念(原始时代的火花)
|
||||
## 💡 设计理念 (原始时代的火花)
|
||||
|
||||
> **千石可乐说:**
|
||||
> - 这个项目最初只是为了给牛牛 bot 添加一点额外的功能,但是功能越写越多,最后决定重写。其目的是为了创造一个活跃在 QQ 群聊的"生命体"。目的并不是为了写一个功能齐全的机器人,而是一个尽可能让人感知到真实的类人存在。
|
||||
@@ -119,41 +116,39 @@
|
||||
> - 如果人类真的需要一个 AI 来陪伴自己,并不是所有人都需要一个完美的,能解决所有问题的"helpful assistant",而是一个会犯错的,拥有自己感知和想法的"生命形式"。
|
||||
> - 代码会保持开源和开放,但个人希望 MaiMbot 的运行时数据保持封闭,尽量避免以显式命令来对其进行控制和调试。我认为一个你无法完全掌控的个体才更能让你感觉到它的自主性,而视其成为一个对话机器。
|
||||
> - SengokuCola~~纯编程外行,面向 cursor 编程,很多代码写得不好多多包涵~~已得到大脑升级。
|
||||
> *Code is open, but the soul is yours.*
|
||||
|
||||
---
|
||||
|
||||
## 🙋 贡献和致谢
|
||||
你可以阅读[开发文档](https://docs.mai-mai.org/develop/)来更好的了解麦麦!
|
||||
MaiCore 是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交 bug 报告、功能需求还是代码 pr,都对项目非常宝贵。我们非常感谢你的支持!🎉
|
||||
但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](docs-src/CONTRIBUTE.md)。(待补完)
|
||||
|
||||
### 贡献者
|
||||
欢迎参与贡献!请先阅读 [贡献指南](docs-src/CONTRIBUTE.md)。
|
||||
|
||||
感谢各位大佬!
|
||||
### 🌟 贡献者
|
||||
|
||||
<a href="https://github.com/MaiM-with-u/MaiBot/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=MaiM-with-u/MaiBot" />
|
||||
</a>
|
||||
|
||||
### 致谢
|
||||
### ❤️ 特别致谢
|
||||
|
||||
- [略nd](https://space.bilibili.com/1344099355): 为麦麦绘制人设。
|
||||
- [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现。
|
||||
- **[略nd](https://space.bilibili.com/1344099355)**: 🎨 为麦麦绘制精美人设。
|
||||
- **[NapCat](https://github.com/NapNeko/NapCatQQ)**: 🚀 现代化的基于 NTQQ 的 Bot 协议实现。
|
||||
|
||||
**也感谢每一位给麦麦发展提出宝贵意见与建议的用户,感谢陪伴麦麦走到现在的你们!**
|
||||
---
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
> [!WARNING]
|
||||
> 使用本项目前必须阅读和同意[用户协议](EULA.md)和[隐私协议](PRIVACY.md)。
|
||||
> 本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI 生成内容不代表本项目团队的观点和立场。
|
||||
|
||||
## 麦麦仓库状态
|
||||
## 📊 仓库状态
|
||||
|
||||

|
||||
|
||||
### Star 趋势
|
||||
|
||||
[](https://starchart.cc/MaiM-with-u/MaiBot)
|
||||
|
||||
## License
|
||||
---
|
||||
|
||||
GPL-3.0
|
||||
## 📌 注意事项 & License
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 使用前请阅读 [用户协议 (EULA)](EULA.md) 和 [隐私协议](PRIVACY.md)。AI 生成内容请仔细甄别。
|
||||
|
||||
**License**: GPL-3.0
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
# Changelog
|
||||
## [0.12.2] - 2025-1-11
|
||||
### 功能更改
|
||||
- 优化私聊wait逻辑
|
||||
- 超时时强制引用回复
|
||||
- 修复部分适配器断联问题
|
||||
- 修复表达反思配置未生效
|
||||
- 优化记忆检索逻辑
|
||||
- 更新readme
|
||||
|
||||
## [0.12.1] - 2025-12-31
|
||||
### 🌟 主要更新
|
||||
- 添加年度总结!可以在webui查看
|
||||
|
||||
BIN
depends-data/maimai-v2.png
Normal file
BIN
depends-data/maimai-v2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1,507 +0,0 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import importlib
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
# 强制使用 utf-8,避免控制台编码报错
|
||||
try:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 确保能导入 src.*
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from src.common.logger import initialize_logging, get_logger
|
||||
from src.common.database.database import db
|
||||
from src.common.database.database_model import LLMUsage
|
||||
|
||||
logger = get_logger("compare_finish_search_token")
|
||||
|
||||
|
||||
def get_token_usage_since(start_time: float) -> Dict[str, Any]:
|
||||
"""获取从指定时间开始的token使用情况
|
||||
|
||||
Args:
|
||||
start_time: 开始时间戳
|
||||
|
||||
Returns:
|
||||
包含token使用统计的字典
|
||||
"""
|
||||
try:
|
||||
start_datetime = datetime.fromtimestamp(start_time)
|
||||
|
||||
# 查询从开始时间到现在的所有memory相关的token使用记录
|
||||
records = (
|
||||
LLMUsage.select()
|
||||
.where(
|
||||
(LLMUsage.timestamp >= start_datetime)
|
||||
& (
|
||||
(LLMUsage.request_type.like("%memory%"))
|
||||
| (LLMUsage.request_type == "memory.question")
|
||||
| (LLMUsage.request_type == "memory.react")
|
||||
| (LLMUsage.request_type == "memory.react.final")
|
||||
)
|
||||
)
|
||||
.order_by(LLMUsage.timestamp.asc())
|
||||
)
|
||||
|
||||
total_prompt_tokens = 0
|
||||
total_completion_tokens = 0
|
||||
total_tokens = 0
|
||||
total_cost = 0.0
|
||||
request_count = 0
|
||||
model_usage = {} # 按模型统计
|
||||
|
||||
for record in records:
|
||||
total_prompt_tokens += record.prompt_tokens or 0
|
||||
total_completion_tokens += record.completion_tokens or 0
|
||||
total_tokens += record.total_tokens or 0
|
||||
total_cost += record.cost or 0.0
|
||||
request_count += 1
|
||||
|
||||
# 按模型统计
|
||||
model_name = record.model_name or "unknown"
|
||||
if model_name not in model_usage:
|
||||
model_usage[model_name] = {
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"cost": 0.0,
|
||||
"request_count": 0,
|
||||
}
|
||||
model_usage[model_name]["prompt_tokens"] += record.prompt_tokens or 0
|
||||
model_usage[model_name]["completion_tokens"] += record.completion_tokens or 0
|
||||
model_usage[model_name]["total_tokens"] += record.total_tokens or 0
|
||||
model_usage[model_name]["cost"] += record.cost or 0.0
|
||||
model_usage[model_name]["request_count"] += 1
|
||||
|
||||
return {
|
||||
"total_prompt_tokens": total_prompt_tokens,
|
||||
"total_completion_tokens": total_completion_tokens,
|
||||
"total_tokens": total_tokens,
|
||||
"total_cost": total_cost,
|
||||
"request_count": request_count,
|
||||
"model_usage": model_usage,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取token使用情况失败: {e}")
|
||||
return {
|
||||
"total_prompt_tokens": 0,
|
||||
"total_completion_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"total_cost": 0.0,
|
||||
"request_count": 0,
|
||||
"model_usage": {},
|
||||
}
|
||||
|
||||
|
||||
def _import_memory_retrieval():
|
||||
"""使用 importlib 动态导入 memory_retrieval 模块,避免循环导入"""
|
||||
try:
|
||||
# 先导入 prompt_builder,检查 prompt 是否已经初始化
|
||||
from src.chat.utils.prompt_builder import global_prompt_manager
|
||||
|
||||
# 检查 memory_retrieval 相关的 prompt 是否已经注册
|
||||
# 如果已经注册,说明模块可能已经通过其他路径初始化过了
|
||||
prompt_already_init = "memory_retrieval_question_prompt" in global_prompt_manager._prompts
|
||||
|
||||
module_name = "src.memory_system.memory_retrieval"
|
||||
|
||||
# 如果 prompt 已经初始化,尝试直接使用已加载的模块
|
||||
if prompt_already_init and module_name in sys.modules:
|
||||
existing_module = sys.modules[module_name]
|
||||
if hasattr(existing_module, 'init_memory_retrieval_prompt'):
|
||||
return (
|
||||
existing_module.init_memory_retrieval_prompt,
|
||||
existing_module._react_agent_solve_question,
|
||||
)
|
||||
|
||||
# 如果模块已经在 sys.modules 中但部分初始化,先移除它
|
||||
if module_name in sys.modules:
|
||||
existing_module = sys.modules[module_name]
|
||||
if not hasattr(existing_module, 'init_memory_retrieval_prompt'):
|
||||
# 模块部分初始化,移除它
|
||||
logger.warning(f"检测到部分初始化的模块 {module_name},尝试重新导入")
|
||||
del sys.modules[module_name]
|
||||
# 清理可能相关的部分初始化模块
|
||||
keys_to_remove = []
|
||||
for key in sys.modules.keys():
|
||||
if key.startswith('src.memory_system.') and key != 'src.memory_system':
|
||||
keys_to_remove.append(key)
|
||||
for key in keys_to_remove:
|
||||
try:
|
||||
del sys.modules[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# 在导入 memory_retrieval 之前,先确保所有可能触发循环导入的模块都已完全加载
|
||||
# 这些模块在导入时可能会触发 memory_retrieval 的导入,所以我们需要先加载它们
|
||||
try:
|
||||
# 先导入可能触发循环导入的模块,让它们完成初始化
|
||||
import src.config.config
|
||||
import src.chat.utils.prompt_builder
|
||||
# 尝试导入可能触发循环导入的模块(这些模块可能在模块级别导入了 memory_retrieval)
|
||||
# 如果它们已经导入,就确保它们完全初始化
|
||||
try:
|
||||
import src.chat.replyer.group_generator # noqa: F401
|
||||
except (ImportError, AttributeError):
|
||||
pass # 如果导入失败,继续
|
||||
try:
|
||||
import src.chat.replyer.private_generator # noqa: F401
|
||||
except (ImportError, AttributeError):
|
||||
pass # 如果导入失败,继续
|
||||
except Exception as e:
|
||||
logger.warning(f"预加载依赖模块时出现警告: {e}")
|
||||
|
||||
# 现在尝试导入 memory_retrieval
|
||||
# 如果此时仍然触发循环导入,说明有其他模块在模块级别导入了 memory_retrieval
|
||||
memory_retrieval_module = importlib.import_module(module_name)
|
||||
|
||||
return (
|
||||
memory_retrieval_module.init_memory_retrieval_prompt,
|
||||
memory_retrieval_module._react_agent_solve_question,
|
||||
)
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.error(f"导入 memory_retrieval 模块失败: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def _init_tools_without_finish_search():
|
||||
"""初始化工具但不注册 finish_search"""
|
||||
from src.memory_system.retrieval_tools import (
|
||||
register_query_chat_history,
|
||||
register_query_person_info,
|
||||
register_query_words,
|
||||
)
|
||||
from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
|
||||
from src.config.config import global_config
|
||||
|
||||
# 清空工具注册器
|
||||
tool_registry = get_tool_registry()
|
||||
tool_registry.tools.clear()
|
||||
|
||||
# 注册除 finish_search 外的所有工具
|
||||
register_query_chat_history()
|
||||
register_query_person_info()
|
||||
register_query_words()
|
||||
|
||||
# 如果启用 LPMM agent 模式,也注册 LPMM 工具
|
||||
if global_config.lpmm_knowledge.lpmm_mode == "agent":
|
||||
from src.memory_system.retrieval_tools.query_lpmm_knowledge import register_tool as register_lpmm_knowledge
|
||||
register_lpmm_knowledge()
|
||||
|
||||
logger.info("已初始化工具(不包含 finish_search)")
|
||||
|
||||
|
||||
def _init_tools_with_finish_search():
|
||||
"""初始化工具并注册 finish_search"""
|
||||
from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
|
||||
from src.memory_system.retrieval_tools import init_all_tools
|
||||
|
||||
# 清空工具注册器
|
||||
tool_registry = get_tool_registry()
|
||||
tool_registry.tools.clear()
|
||||
|
||||
# 初始化所有工具(包括 finish_search)
|
||||
init_all_tools()
|
||||
logger.info("已初始化工具(包含 finish_search)")
|
||||
|
||||
|
||||
async def get_prompt_tokens_for_tools(
|
||||
question: str,
|
||||
chat_id: str,
|
||||
use_finish_search: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取使用不同工具配置时的prompt token消耗
|
||||
|
||||
Args:
|
||||
question: 要查询的问题
|
||||
chat_id: 聊天ID
|
||||
use_finish_search: 是否使用 finish_search 工具
|
||||
|
||||
Returns:
|
||||
包含prompt token信息的字典
|
||||
"""
|
||||
# 先初始化 prompt(如果还未初始化)
|
||||
# 注意:init_memory_retrieval_prompt 会调用 init_all_tools,所以我们需要在它之后重新设置工具
|
||||
from src.chat.utils.prompt_builder import global_prompt_manager
|
||||
if "memory_retrieval_question_prompt" not in global_prompt_manager._prompts:
|
||||
init_memory_retrieval_prompt, _ = _import_memory_retrieval()
|
||||
init_memory_retrieval_prompt()
|
||||
|
||||
# 初始化工具(根据参数决定是否包含 finish_search)
|
||||
# 必须在 init_memory_retrieval_prompt 之后调用,因为它会调用 init_all_tools
|
||||
if use_finish_search:
|
||||
_init_tools_with_finish_search()
|
||||
else:
|
||||
_init_tools_without_finish_search()
|
||||
|
||||
# 获取工具注册器
|
||||
from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
|
||||
tool_registry = get_tool_registry()
|
||||
tool_definitions = tool_registry.get_tool_definitions()
|
||||
|
||||
# 验证工具列表(调试用)
|
||||
tool_names = [tool["name"] for tool in tool_definitions]
|
||||
if use_finish_search:
|
||||
if "finish_search" not in tool_names:
|
||||
logger.warning("期望包含 finish_search 工具,但工具列表中未找到")
|
||||
else:
|
||||
if "finish_search" in tool_names:
|
||||
logger.warning("期望不包含 finish_search 工具,但工具列表中找到了,将移除")
|
||||
# 移除 finish_search 工具
|
||||
tool_registry.tools.pop("finish_search", None)
|
||||
tool_definitions = tool_registry.get_tool_definitions()
|
||||
tool_names = [tool["name"] for tool in tool_definitions]
|
||||
|
||||
# 构建第一次调用的prompt(模拟_react_agent_solve_question的第一次调用)
|
||||
from src.config.config import global_config
|
||||
bot_name = global_config.bot.nickname
|
||||
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
|
||||
# 构建head_prompt
|
||||
head_prompt = await global_prompt_manager.format_prompt(
|
||||
"memory_retrieval_react_prompt_head",
|
||||
bot_name=bot_name,
|
||||
time_now=time_now,
|
||||
question=question,
|
||||
collected_info="",
|
||||
current_iteration=1,
|
||||
remaining_iterations=global_config.memory.max_agent_iterations - 1,
|
||||
max_iterations=global_config.memory.max_agent_iterations,
|
||||
)
|
||||
|
||||
# 构建消息列表(只包含system message,模拟第一次调用)
|
||||
from src.llm_models.payload_content.message import MessageBuilder, RoleType
|
||||
messages = []
|
||||
system_builder = MessageBuilder()
|
||||
system_builder.set_role(RoleType.System)
|
||||
system_builder.add_text_content(head_prompt)
|
||||
messages.append(system_builder.build())
|
||||
|
||||
# 调用LLM API来计算token(只调用一次,不实际执行)
|
||||
from src.llm_models.utils_model import LLMRequest, RequestType
|
||||
from src.config.config import model_config
|
||||
|
||||
# 创建LLM请求对象
|
||||
llm_request = LLMRequest(model_set=model_config.model_task_config.tool_use, request_type="memory.react.compare")
|
||||
|
||||
# 构建工具选项
|
||||
tool_built = llm_request._build_tool_options(tool_definitions)
|
||||
|
||||
# 直接调用 _execute_request 以获取完整的响应对象(包含 usage)
|
||||
response, model_info = await llm_request._execute_request(
|
||||
request_type=RequestType.RESPONSE,
|
||||
message_factory=lambda _client, *, _messages=messages: _messages,
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
tool_options=tool_built,
|
||||
)
|
||||
|
||||
# 从响应中获取token使用情况
|
||||
prompt_tokens = 0
|
||||
completion_tokens = 0
|
||||
total_tokens = 0
|
||||
|
||||
if response and hasattr(response, 'usage') and response.usage:
|
||||
prompt_tokens = response.usage.prompt_tokens or 0
|
||||
completion_tokens = response.usage.completion_tokens or 0
|
||||
total_tokens = response.usage.total_tokens or 0
|
||||
|
||||
return {
|
||||
"use_finish_search": use_finish_search,
|
||||
"prompt_tokens": prompt_tokens,
|
||||
"completion_tokens": completion_tokens,
|
||||
"total_tokens": total_tokens,
|
||||
"tool_count": len(tool_definitions),
|
||||
"tool_names": [tool["name"] for tool in tool_definitions],
|
||||
}
|
||||
|
||||
|
||||
async def compare_prompt_tokens(
|
||||
question: str,
|
||||
chat_id: str = "compare_finish_search",
|
||||
) -> Dict[str, Any]:
|
||||
"""对比使用 finish_search 工具与否的输入 token 差异
|
||||
|
||||
只运行一次,只计算输入 token 的差异,确保除了工具定义外其他内容一致
|
||||
|
||||
Args:
|
||||
question: 要查询的问题
|
||||
chat_id: 聊天ID
|
||||
|
||||
Returns:
|
||||
包含对比结果的字典
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("finish_search 工具 输入 Token 消耗对比测试")
|
||||
print("=" * 80)
|
||||
print(f"\n[测试问题] {question}")
|
||||
print(f"[聊天ID] {chat_id}")
|
||||
print("\n注意: 只对比第一次LLM调用的输入token差异,不运行完整迭代流程")
|
||||
|
||||
# 第一次测试:不使用 finish_search
|
||||
print("\n" + "-" * 80)
|
||||
print("[测试 1/2] 不使用 finish_search 工具")
|
||||
print("-" * 80)
|
||||
result_without = await get_prompt_tokens_for_tools(
|
||||
question=question,
|
||||
chat_id=f"{chat_id}_without",
|
||||
use_finish_search=False,
|
||||
)
|
||||
|
||||
print(f"\n[结果]")
|
||||
print(f" 工具数量: {result_without['tool_count']}")
|
||||
print(f" 工具列表: {', '.join(result_without['tool_names'])}")
|
||||
print(f" 输入 Prompt Tokens: {result_without['prompt_tokens']:,}")
|
||||
|
||||
# 等待一下,确保数据库记录已写入
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 第二次测试:使用 finish_search
|
||||
print("\n" + "-" * 80)
|
||||
print("[测试 2/2] 使用 finish_search 工具")
|
||||
print("-" * 80)
|
||||
result_with = await get_prompt_tokens_for_tools(
|
||||
question=question,
|
||||
chat_id=f"{chat_id}_with",
|
||||
use_finish_search=True,
|
||||
)
|
||||
|
||||
print(f"\n[结果]")
|
||||
print(f" 工具数量: {result_with['tool_count']}")
|
||||
print(f" 工具列表: {', '.join(result_with['tool_names'])}")
|
||||
print(f" 输入 Prompt Tokens: {result_with['prompt_tokens']:,}")
|
||||
|
||||
# 对比结果
|
||||
print("\n" + "=" * 80)
|
||||
print("[对比结果]")
|
||||
print("=" * 80)
|
||||
|
||||
prompt_token_diff = result_with['prompt_tokens'] - result_without['prompt_tokens']
|
||||
prompt_token_diff_percent = (prompt_token_diff / result_without['prompt_tokens'] * 100) if result_without['prompt_tokens'] > 0 else 0
|
||||
|
||||
tool_count_diff = result_with['tool_count'] - result_without['tool_count']
|
||||
|
||||
print(f"\n[输入 Prompt Token 对比]")
|
||||
print(f" 不使用 finish_search: {result_without['prompt_tokens']:,} tokens")
|
||||
print(f" 使用 finish_search: {result_with['prompt_tokens']:,} tokens")
|
||||
print(f" 差异: {prompt_token_diff:+,} tokens ({prompt_token_diff_percent:+.2f}%)")
|
||||
|
||||
print(f"\n[工具数量对比]")
|
||||
print(f" 不使用 finish_search: {result_without['tool_count']} 个工具")
|
||||
print(f" 使用 finish_search: {result_with['tool_count']} 个工具")
|
||||
print(f" 差异: {tool_count_diff:+d} 个工具")
|
||||
|
||||
print(f"\n[工具列表对比]")
|
||||
without_tools = set(result_without['tool_names'])
|
||||
with_tools = set(result_with['tool_names'])
|
||||
only_with = with_tools - without_tools
|
||||
only_without = without_tools - with_tools
|
||||
|
||||
if only_with:
|
||||
print(f" 仅在 '使用 finish_search' 中的工具: {', '.join(only_with)}")
|
||||
if only_without:
|
||||
print(f" 仅在 '不使用 finish_search' 中的工具: {', '.join(only_without)}")
|
||||
if not only_with and not only_without:
|
||||
print(f" 工具列表相同(除了 finish_search)")
|
||||
|
||||
# 显示其他token信息
|
||||
print(f"\n[其他 Token 信息]")
|
||||
print(f" Completion Tokens (不使用 finish_search): {result_without.get('completion_tokens', 0):,}")
|
||||
print(f" Completion Tokens (使用 finish_search): {result_with.get('completion_tokens', 0):,}")
|
||||
print(f" 总 Tokens (不使用 finish_search): {result_without.get('total_tokens', 0):,}")
|
||||
print(f" 总 Tokens (使用 finish_search): {result_with.get('total_tokens', 0):,}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
return {
|
||||
"question": question,
|
||||
"without_finish_search": result_without,
|
||||
"with_finish_search": result_with,
|
||||
"comparison": {
|
||||
"prompt_token_diff": prompt_token_diff,
|
||||
"prompt_token_diff_percent": prompt_token_diff_percent,
|
||||
"tool_count_diff": tool_count_diff,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="对比使用 finish_search 工具与否的 token 消耗差异"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--chat-id",
|
||||
default="compare_finish_search",
|
||||
help="测试用的聊天ID(默认: compare_finish_search)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="将结果保存到JSON文件(可选)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化日志(使用较低的详细程度,避免输出过多日志)
|
||||
initialize_logging(verbose=False)
|
||||
|
||||
# 交互式输入问题
|
||||
print("\n" + "=" * 80)
|
||||
print("finish_search 工具 Token 消耗对比测试工具")
|
||||
print("=" * 80)
|
||||
question = input("\n请输入要查询的问题: ").strip()
|
||||
if not question:
|
||||
print("错误: 问题不能为空")
|
||||
return
|
||||
|
||||
# 连接数据库
|
||||
try:
|
||||
db.connect(reuse_if_open=True)
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接失败: {e}")
|
||||
print(f"错误: 数据库连接失败: {e}")
|
||||
return
|
||||
|
||||
# 运行对比测试
|
||||
try:
|
||||
result = asyncio.run(
|
||||
compare_prompt_tokens(
|
||||
question=question,
|
||||
chat_id=args.chat_id,
|
||||
)
|
||||
)
|
||||
|
||||
# 如果指定了输出文件,保存结果
|
||||
if args.output:
|
||||
# 将thinking_steps转换为可序列化的格式
|
||||
output_result = result.copy()
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(output_result, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n[结果已保存] {args.output}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[中断] 用户中断测试")
|
||||
except Exception as e:
|
||||
logger.error(f"测试失败: {e}", exc_info=True)
|
||||
print(f"\n[错误] 测试失败: {e}")
|
||||
finally:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,476 +0,0 @@
|
||||
"""
|
||||
表达方式评估脚本
|
||||
|
||||
功能:
|
||||
1. 随机读取指定数量的表达方式,获取其situation和style
|
||||
2. 先进行人工评估(逐条手动评估)
|
||||
3. 然后使用LLM进行评估
|
||||
4. 对比人工评估和LLM评估的正确率、精确率、召回率、F1分数等指标(以人工评估为标准)
|
||||
5. 不真正修改数据库,只是做评估
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import List, Dict
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from src.common.database.database_model import Expression
|
||||
from src.common.database.database import db
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import model_config
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("expression_evaluator_comparison")
|
||||
|
||||
|
||||
def get_random_expressions(count: int = 10) -> List[Expression]:
|
||||
"""
|
||||
随机读取指定数量的表达方式
|
||||
|
||||
Args:
|
||||
count: 要读取的数量,默认10条
|
||||
|
||||
Returns:
|
||||
表达方式列表
|
||||
"""
|
||||
try:
|
||||
# 查询所有表达方式
|
||||
all_expressions = list(Expression.select())
|
||||
|
||||
if not all_expressions:
|
||||
logger.warning("数据库中没有表达方式记录")
|
||||
return []
|
||||
|
||||
# 如果总数少于请求数量,返回所有
|
||||
if len(all_expressions) <= count:
|
||||
logger.info(f"数据库中共有 {len(all_expressions)} 条表达方式,全部返回")
|
||||
return all_expressions
|
||||
|
||||
# 随机选择指定数量
|
||||
selected = random.sample(all_expressions, count)
|
||||
logger.info(f"从 {len(all_expressions)} 条表达方式中随机选择了 {len(selected)} 条")
|
||||
return selected
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"随机读取表达方式失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return []
|
||||
|
||||
|
||||
def manual_evaluate_expression(expression: Expression, index: int, total: int) -> Dict:
|
||||
"""
|
||||
人工评估单个表达方式
|
||||
|
||||
Args:
|
||||
expression: 表达方式对象
|
||||
index: 当前索引(从1开始)
|
||||
total: 总数
|
||||
|
||||
Returns:
|
||||
评估结果字典,包含:
|
||||
- expression_id: 表达方式ID
|
||||
- situation: 情境
|
||||
- style: 风格
|
||||
- suitable: 是否合适(人工评估)
|
||||
- reason: 评估理由(始终为None)
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print(f"人工评估 [{index}/{total}]")
|
||||
print("=" * 60)
|
||||
print(f"Situation: {expression.situation}")
|
||||
print(f"Style: {expression.style}")
|
||||
print("\n请评估该表达方式是否合适:")
|
||||
print(" 输入 'y' 或 'yes' 或 '1' 表示合适(通过)")
|
||||
print(" 输入 'n' 或 'no' 或 '0' 表示不合适(不通过)")
|
||||
print(" 输入 'q' 或 'quit' 退出评估")
|
||||
|
||||
while True:
|
||||
user_input = input("\n您的评估 (y/n/q): ").strip().lower()
|
||||
|
||||
if user_input in ['q', 'quit']:
|
||||
print("退出评估")
|
||||
return None
|
||||
|
||||
if user_input in ['y', 'yes', '1', '是', '通过']:
|
||||
suitable = True
|
||||
break
|
||||
elif user_input in ['n', 'no', '0', '否', '不通过']:
|
||||
suitable = False
|
||||
break
|
||||
else:
|
||||
print("输入无效,请重新输入 (y/n/q)")
|
||||
|
||||
result = {
|
||||
"expression_id": expression.id,
|
||||
"situation": expression.situation,
|
||||
"style": expression.style,
|
||||
"suitable": suitable,
|
||||
"reason": None,
|
||||
"evaluator": "manual"
|
||||
}
|
||||
|
||||
print(f"\n✓ 已记录:{'通过' if suitable else '不通过'}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_evaluation_prompt(situation: str, style: str) -> str:
|
||||
"""
|
||||
创建评估提示词
|
||||
|
||||
Args:
|
||||
situation: 情境
|
||||
style: 风格
|
||||
|
||||
Returns:
|
||||
评估提示词
|
||||
"""
|
||||
prompt = f"""请评估以下表达方式是否合适:
|
||||
|
||||
情境(situation):{situation}
|
||||
风格(style):{style}
|
||||
|
||||
请从以下方面进行评估:
|
||||
1. 情境描述是否清晰、准确
|
||||
2. 风格表达是否合理、自然
|
||||
3. 情境和风格是否匹配
|
||||
4. 允许部分语法错误出现
|
||||
5. 允许口头化或缺省表达
|
||||
6. 允许部分上下文缺失
|
||||
|
||||
请以JSON格式输出评估结果:
|
||||
{{
|
||||
"suitable": true/false,
|
||||
"reason": "评估理由(如果不合适,请说明原因)"
|
||||
}}
|
||||
|
||||
如果合适,suitable设为true;如果不合适,suitable设为false,并在reason中说明原因。
|
||||
请严格按照JSON格式输出,不要包含其他内容。"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
async def _single_llm_evaluation(expression: Expression, llm: LLMRequest) -> tuple[bool, str, str | None]:
|
||||
"""
|
||||
执行单次LLM评估
|
||||
|
||||
Args:
|
||||
expression: 表达方式对象
|
||||
llm: LLM请求实例
|
||||
|
||||
Returns:
|
||||
(suitable, reason, error) 元组,如果出错则 suitable 为 False,error 包含错误信息
|
||||
"""
|
||||
try:
|
||||
prompt = create_evaluation_prompt(expression.situation, expression.style)
|
||||
logger.debug(f"正在评估表达方式 ID: {expression.id}")
|
||||
|
||||
response, (reasoning, model_name, _) = await llm.generate_response_async(
|
||||
prompt=prompt,
|
||||
temperature=0.6,
|
||||
max_tokens=1024
|
||||
)
|
||||
|
||||
logger.debug(f"LLM响应: {response}")
|
||||
|
||||
# 解析JSON响应
|
||||
try:
|
||||
evaluation = json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
import re
|
||||
json_match = re.search(r'\{[^{}]*"suitable"[^{}]*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
evaluation = json.loads(json_match.group())
|
||||
else:
|
||||
raise ValueError("无法从响应中提取JSON格式的评估结果")
|
||||
|
||||
suitable = evaluation.get("suitable", False)
|
||||
reason = evaluation.get("reason", "未提供理由")
|
||||
|
||||
logger.debug(f"评估结果: {'通过' if suitable else '不通过'}")
|
||||
return suitable, reason, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"评估表达方式 ID: {expression.id} 时出错: {e}")
|
||||
return False, f"评估过程出错: {str(e)}", str(e)
|
||||
|
||||
|
||||
async def evaluate_expression_llm(expression: Expression, llm: LLMRequest) -> Dict:
|
||||
"""
|
||||
使用LLM评估单个表达方式
|
||||
|
||||
Args:
|
||||
expression: 表达方式对象
|
||||
llm: LLM请求实例
|
||||
|
||||
Returns:
|
||||
评估结果字典
|
||||
"""
|
||||
logger.info(f"开始评估表达方式 ID: {expression.id}")
|
||||
|
||||
suitable, reason, error = await _single_llm_evaluation(expression, llm)
|
||||
|
||||
if error:
|
||||
suitable = False
|
||||
|
||||
logger.info(f"评估完成: {'通过' if suitable else '不通过'}")
|
||||
|
||||
return {
|
||||
"expression_id": expression.id,
|
||||
"situation": expression.situation,
|
||||
"style": expression.style,
|
||||
"suitable": suitable,
|
||||
"reason": reason,
|
||||
"error": error,
|
||||
"evaluator": "llm"
|
||||
}
|
||||
|
||||
|
||||
def compare_evaluations(manual_results: List[Dict], llm_results: List[Dict], method_name: str) -> Dict:
|
||||
"""
|
||||
对比人工评估和LLM评估的结果
|
||||
|
||||
Args:
|
||||
manual_results: 人工评估结果列表
|
||||
llm_results: LLM评估结果列表
|
||||
method_name: 评估方法名称(用于标识)
|
||||
|
||||
Returns:
|
||||
对比分析结果字典
|
||||
"""
|
||||
# 按expression_id建立映射
|
||||
llm_dict = {r["expression_id"]: r for r in llm_results}
|
||||
|
||||
total = len(manual_results)
|
||||
matched = 0
|
||||
true_positives = 0
|
||||
true_negatives = 0
|
||||
false_positives = 0
|
||||
false_negatives = 0
|
||||
|
||||
for manual_result in manual_results:
|
||||
llm_result = llm_dict.get(manual_result["expression_id"])
|
||||
if llm_result is None:
|
||||
continue
|
||||
|
||||
manual_suitable = manual_result["suitable"]
|
||||
llm_suitable = llm_result["suitable"]
|
||||
|
||||
if manual_suitable == llm_suitable:
|
||||
matched += 1
|
||||
|
||||
if manual_suitable and llm_suitable:
|
||||
true_positives += 1
|
||||
elif not manual_suitable and not llm_suitable:
|
||||
true_negatives += 1
|
||||
elif not manual_suitable and llm_suitable:
|
||||
false_positives += 1
|
||||
elif manual_suitable and not llm_suitable:
|
||||
false_negatives += 1
|
||||
|
||||
accuracy = (matched / total * 100) if total > 0 else 0
|
||||
precision = (true_positives / (true_positives + false_positives) * 100) if (true_positives + false_positives) > 0 else 0
|
||||
recall = (true_positives / (true_positives + false_negatives) * 100) if (true_positives + false_negatives) > 0 else 0
|
||||
f1_score = (2 * precision * recall / (precision + recall)) if (precision + recall) > 0 else 0
|
||||
specificity = (true_negatives / (true_negatives + false_positives) * 100) if (true_negatives + false_positives) > 0 else 0
|
||||
|
||||
random_baseline = 50.0
|
||||
accuracy_above_random = accuracy - random_baseline
|
||||
accuracy_improvement_ratio = (accuracy / random_baseline) if random_baseline > 0 else 0
|
||||
|
||||
return {
|
||||
"method": method_name,
|
||||
"total": total,
|
||||
"matched": matched,
|
||||
"accuracy": accuracy,
|
||||
"accuracy_above_random": accuracy_above_random,
|
||||
"accuracy_improvement_ratio": accuracy_improvement_ratio,
|
||||
"true_positives": true_positives,
|
||||
"true_negatives": true_negatives,
|
||||
"false_positives": false_positives,
|
||||
"false_negatives": false_negatives,
|
||||
"precision": precision,
|
||||
"recall": recall,
|
||||
"f1_score": f1_score,
|
||||
"specificity": specificity
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("开始表达方式评估")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 初始化数据库连接
|
||||
try:
|
||||
db.connect(reuse_if_open=True)
|
||||
logger.info("数据库连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接失败: {e}")
|
||||
return
|
||||
|
||||
# 1. 随机读取表达方式
|
||||
logger.info("\n步骤1: 随机读取表达方式")
|
||||
expressions = get_random_expressions(10)
|
||||
if not expressions:
|
||||
logger.error("没有可用的表达方式,退出")
|
||||
return
|
||||
logger.info(f"成功读取 {len(expressions)} 条表达方式")
|
||||
|
||||
# 2. 人工评估
|
||||
print("\n" + "=" * 60)
|
||||
print("开始人工评估")
|
||||
print("=" * 60)
|
||||
print(f"共需要评估 {len(expressions)} 条表达方式")
|
||||
print("请逐条进行评估...\n")
|
||||
|
||||
manual_results = []
|
||||
for i, expression in enumerate(expressions, 1):
|
||||
manual_result = manual_evaluate_expression(expression, i, len(expressions))
|
||||
if manual_result is None:
|
||||
print("\n评估已中断")
|
||||
return
|
||||
manual_results.append(manual_result)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("人工评估完成")
|
||||
print("=" * 60)
|
||||
|
||||
# 3. 创建LLM实例并评估
|
||||
logger.info("\n步骤3: 创建LLM实例")
|
||||
try:
|
||||
llm = LLMRequest(
|
||||
model_set=model_config.model_task_config.tool_use,
|
||||
request_type="expression_evaluator_comparison"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"创建LLM实例失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return
|
||||
|
||||
logger.info("\n步骤4: 开始LLM评估")
|
||||
llm_results = []
|
||||
for i, expression in enumerate(expressions, 1):
|
||||
logger.info(f"LLM评估进度: {i}/{len(expressions)}")
|
||||
llm_results.append(await evaluate_expression_llm(expression, llm))
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# 4. 对比分析并输出结果
|
||||
comparison = compare_evaluations(manual_results, llm_results, "LLM评估")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("评估结果(以人工评估为标准)")
|
||||
print("=" * 60)
|
||||
print("\n评估目标:")
|
||||
print(" 1. 核心能力:将不合适的项目正确提取出来(特定负类召回率)")
|
||||
print(" 2. 次要能力:尽可能少的误删合适的项目(召回率)")
|
||||
|
||||
# 详细评估结果(核心指标优先)
|
||||
print("\n【详细对比】")
|
||||
print(f"\n--- {comparison['method']} ---")
|
||||
print(f" 总数: {comparison['total']} 条")
|
||||
print()
|
||||
print(" 【核心能力指标】")
|
||||
print(f" ⭐ 特定负类召回率: {comparison['specificity']:.2f}% (将不合适项目正确提取出来的能力)")
|
||||
print(f" - 计算: TN / (TN + FP) = {comparison['true_negatives']} / ({comparison['true_negatives']} + {comparison['false_positives']})")
|
||||
print(f" - 含义: 在 {comparison['true_negatives'] + comparison['false_positives']} 个实际不合适的项目中,正确识别出 {comparison['true_negatives']} 个")
|
||||
print(f" - 随机水平: 50.00% (当前高于随机: {comparison['specificity'] - 50.0:+.2f}%)")
|
||||
print()
|
||||
print(f" ⭐ 召回率: {comparison['recall']:.2f}% (尽可能少的误删合适项目的能力)")
|
||||
print(f" - 计算: TP / (TP + FN) = {comparison['true_positives']} / ({comparison['true_positives']} + {comparison['false_negatives']})")
|
||||
print(f" - 含义: 在 {comparison['true_positives'] + comparison['false_negatives']} 个实际合适的项目中,正确识别出 {comparison['true_positives']} 个")
|
||||
print(f" - 随机水平: 50.00% (当前高于随机: {comparison['recall'] - 50.0:+.2f}%)")
|
||||
print()
|
||||
print(" 【其他指标】")
|
||||
print(f" 准确率: {comparison['accuracy']:.2f}% (整体判断正确率)")
|
||||
print(f" 精确率: {comparison['precision']:.2f}% (判断为合适的项目中,实际合适的比例)")
|
||||
print(f" F1分数: {comparison['f1_score']:.2f} (精确率和召回率的调和平均)")
|
||||
print(f" 匹配数: {comparison['matched']}/{comparison['total']}")
|
||||
print()
|
||||
print(" 【分类统计】")
|
||||
print(f" TP (正确识别为合适): {comparison['true_positives']}")
|
||||
print(f" TN (正确识别为不合适): {comparison['true_negatives']} ⭐")
|
||||
print(f" FP (误判为合适): {comparison['false_positives']} ⚠️")
|
||||
print(f" FN (误删合适项目): {comparison['false_negatives']} ⚠️")
|
||||
|
||||
# 5. 输出人工评估不通过但LLM误判为通过的详细信息
|
||||
print("\n" + "=" * 60)
|
||||
print("人工评估不通过但LLM误判为通过的项目(FP - False Positive)")
|
||||
print("=" * 60)
|
||||
|
||||
# 按expression_id建立映射
|
||||
llm_dict = {r["expression_id"]: r for r in llm_results}
|
||||
|
||||
fp_items = []
|
||||
for manual_result in manual_results:
|
||||
llm_result = llm_dict.get(manual_result["expression_id"])
|
||||
if llm_result is None:
|
||||
continue
|
||||
|
||||
# 人工评估不通过,但LLM评估通过(FP情况)
|
||||
if not manual_result["suitable"] and llm_result["suitable"]:
|
||||
fp_items.append({
|
||||
"expression_id": manual_result["expression_id"],
|
||||
"situation": manual_result["situation"],
|
||||
"style": manual_result["style"],
|
||||
"manual_suitable": manual_result["suitable"],
|
||||
"llm_suitable": llm_result["suitable"],
|
||||
"llm_reason": llm_result.get("reason", "未提供理由"),
|
||||
"llm_error": llm_result.get("error")
|
||||
})
|
||||
|
||||
if fp_items:
|
||||
print(f"\n共找到 {len(fp_items)} 条误判项目:\n")
|
||||
for idx, item in enumerate(fp_items, 1):
|
||||
print(f"--- [{idx}] 项目 ID: {item['expression_id']} ---")
|
||||
print(f"Situation: {item['situation']}")
|
||||
print(f"Style: {item['style']}")
|
||||
print("人工评估: 不通过 ❌")
|
||||
print("LLM评估: 通过 ✅ (误判)")
|
||||
if item.get('llm_error'):
|
||||
print(f"LLM错误: {item['llm_error']}")
|
||||
print(f"LLM理由: {item['llm_reason']}")
|
||||
print()
|
||||
else:
|
||||
print("\n✓ 没有误判项目(所有人工评估不通过的项目都被LLM正确识别为不通过)")
|
||||
|
||||
# 6. 保存结果到JSON文件
|
||||
output_file = os.path.join(project_root, "data", "expression_evaluation_comparison.json")
|
||||
try:
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"manual_results": manual_results,
|
||||
"llm_results": llm_results,
|
||||
"comparison": comparison
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"\n评估结果已保存到: {output_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"保存结果到文件失败: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("评估完成")
|
||||
print("=" * 60)
|
||||
|
||||
# 关闭数据库连接
|
||||
try:
|
||||
db.close()
|
||||
logger.info("数据库连接已关闭")
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭数据库连接时出错: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -28,7 +28,7 @@ class ExpressionReflector:
|
||||
try:
|
||||
logger.debug(f"[Expression Reflection] 开始检查是否需要提问 (stream_id: {self.chat_id})")
|
||||
|
||||
if not global_config.expression.expression_self_reflect:
|
||||
if not global_config.expression.expression_manual_reflect:
|
||||
logger.debug("[Expression Reflection] 表达反思功能未启用,跳过")
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import time
|
||||
from typing import Tuple, Optional # 增加了 Optional
|
||||
from src.common.logger_manager import get_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
import random
|
||||
from .chat_observer import ChatObserver
|
||||
from .pfc_utils import get_items_from_json
|
||||
from src.individuality.individuality import Individuality
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
|
||||
logger = get_logger("pfc_action_planner")
|
||||
@@ -113,12 +113,27 @@ class ActionPlanner:
|
||||
max_tokens=1500,
|
||||
request_type="action_planning",
|
||||
)
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.personality_info = self._get_personality_prompt()
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.private_name = private_name
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
# self.action_planner_info = ActionPlannerInfo() # 移除未使用的变量
|
||||
|
||||
def _get_personality_prompt(self) -> str:
|
||||
"""获取个性提示信息"""
|
||||
prompt_personality = global_config.personality.personality
|
||||
|
||||
# 检查是否需要随机替换为状态
|
||||
if (
|
||||
global_config.personality.states
|
||||
and global_config.personality.state_probability > 0
|
||||
and random.random() < global_config.personality.state_probability
|
||||
):
|
||||
prompt_personality = random.choice(global_config.personality.states)
|
||||
|
||||
bot_name = global_config.BOT_NICKNAME
|
||||
return f"你的名字是{bot_name},你{prompt_personality};"
|
||||
|
||||
# 修改 plan 方法签名,增加 last_successful_reply_action 参数
|
||||
async def plan(
|
||||
self,
|
||||
|
||||
@@ -3,22 +3,22 @@ import asyncio
|
||||
import datetime
|
||||
|
||||
# from .message_storage import MongoDBMessageStorage
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
|
||||
# from ...config.config import global_config
|
||||
# from src.config.config import global_config
|
||||
from typing import Dict, Any, Optional
|
||||
from ..chat.message import Message
|
||||
from src.chat.message_receive.message import Message
|
||||
from .pfc_types import ConversationState
|
||||
from .pfc import ChatObserver, GoalAnalyzer
|
||||
from .message_sender import DirectMessageSender
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.common.logger import get_logger
|
||||
from .action_planner import ActionPlanner
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo # 确保导入 ConversationInfo
|
||||
from .reply_generator import ReplyGenerator
|
||||
from ..chat.chat_stream import ChatStream
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from maim_message import UserInfo
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from .pfc_KnowledgeFetcher import KnowledgeFetcher
|
||||
from .waiter import Waiter
|
||||
|
||||
@@ -60,7 +60,7 @@ class Conversation:
|
||||
self.direct_sender = DirectMessageSender(self.private_name)
|
||||
|
||||
# 获取聊天流信息
|
||||
self.chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
self.chat_stream = get_chat_manager().get_stream(self.stream_id)
|
||||
|
||||
self.stop_action_planner = False
|
||||
except Exception as e:
|
||||
@@ -248,14 +248,14 @@ class Conversation:
|
||||
def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
|
||||
"""将消息字典转换为Message对象"""
|
||||
try:
|
||||
# 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 chat_manager 获取
|
||||
# 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 get_chat_manager 获取
|
||||
chat_info = msg_dict.get("chat_info")
|
||||
if chat_info and isinstance(chat_info, dict):
|
||||
chat_stream = ChatStream.from_dict(chat_info)
|
||||
elif self.chat_stream: # 使用实例变量中的 chat_stream
|
||||
chat_stream = self.chat_stream
|
||||
else: # Fallback: 尝试从 manager 获取 (可能需要 stream_id)
|
||||
chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
chat_stream = get_chat_manager().get_stream(self.stream_id)
|
||||
if not chat_stream:
|
||||
raise ValueError(f"无法确定 ChatStream for stream_id {self.stream_id}")
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
from src.common.logger import get_module_logger
|
||||
from ..chat.chat_stream import ChatStream
|
||||
from ..chat.message import Message
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.message_receive.message import Message, MessageSending
|
||||
from maim_message import UserInfo, Seg
|
||||
from src.plugins.chat.message import MessageSending, MessageSet
|
||||
from src.plugins.chat.message_sender import message_manager
|
||||
from ..storage.storage import MessageStorage
|
||||
from ...config.config import global_config
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
install(extra_lines=3)
|
||||
@@ -66,15 +64,18 @@ class DirectMessageSender:
|
||||
# 处理消息
|
||||
await message.process()
|
||||
|
||||
# 不知道有什么用,先留下来了,和之前那套sender一样
|
||||
_message_json = message.to_dict()
|
||||
|
||||
# 发送消息
|
||||
message_set = MessageSet(chat_stream, message_id)
|
||||
message_set.add_message(message)
|
||||
await message_manager.add_message(message_set)
|
||||
await self.storage.store_message(message, chat_stream)
|
||||
logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
|
||||
# 发送消息(直接调用底层 API)
|
||||
from src.chat.message_receive.uni_message_sender import _send_message
|
||||
|
||||
sent = await _send_message(message, show_log=True)
|
||||
|
||||
if sent:
|
||||
# 存储消息
|
||||
await self.storage.store_message(message, chat_stream)
|
||||
logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
|
||||
else:
|
||||
logger.error(f"[私聊][{self.private_name}]PFC消息发送失败")
|
||||
raise RuntimeError("消息发送失败")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
from src.common.logger import get_module_logger
|
||||
from .chat_observer import ChatObserver
|
||||
from .chat_states import NotificationHandler, NotificationType, Notification
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
import traceback # 导入 traceback 用于调试
|
||||
|
||||
logger = get_module_logger("observation_info")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import List, Tuple, TYPE_CHECKING
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
import random
|
||||
from .chat_observer import ChatObserver
|
||||
from .pfc_utils import get_items_from_json
|
||||
from src.individuality.individuality import Individuality
|
||||
from .conversation_info import ConversationInfo
|
||||
from .observation_info import ObservationInfo
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
from rich.traceback import install
|
||||
|
||||
install(extra_lines=3)
|
||||
@@ -46,7 +46,7 @@ class GoalAnalyzer:
|
||||
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
|
||||
)
|
||||
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.personality_info = self._get_personality_prompt()
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.nick_name = global_config.BOT_ALIAS_NAMES
|
||||
self.private_name = private_name
|
||||
@@ -57,6 +57,21 @@ class GoalAnalyzer:
|
||||
self.max_goals = 3 # 同时保持的最大目标数量
|
||||
self.current_goal_and_reason = None
|
||||
|
||||
def _get_personality_prompt(self) -> str:
|
||||
"""获取个性提示信息"""
|
||||
prompt_personality = global_config.personality.personality
|
||||
|
||||
# 检查是否需要随机替换为状态
|
||||
if (
|
||||
global_config.personality.states
|
||||
and global_config.personality.state_probability > 0
|
||||
and random.random() < global_config.personality.state_probability
|
||||
):
|
||||
prompt_personality = random.choice(global_config.personality.states)
|
||||
|
||||
bot_name = global_config.bot.nickname
|
||||
return f"你的名字是{bot_name},你{prompt_personality};"
|
||||
|
||||
async def analyze_goal(self, conversation_info: ConversationInfo, observation_info: ObservationInfo):
|
||||
"""分析对话历史并设定目标
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import List, Tuple
|
||||
from src.common.logger import get_module_logger
|
||||
from src.plugins.memory_system.Hippocampus import HippocampusManager
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from ..chat.message import Message
|
||||
from ..knowledge.knowledge_lib import qa_manager
|
||||
from ..utils.chat_message_builder import build_readable_messages
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.chat.message_receive.message import Message
|
||||
from src.chat.knowledge.knowledge_lib import qa_manager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
logger = get_module_logger("knowledge_fetcher")
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import json
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from .chat_observer import ChatObserver
|
||||
from maim_message import UserInfo
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
import random
|
||||
from .chat_observer import ChatObserver
|
||||
from .reply_checker import ReplyChecker
|
||||
from src.individuality.individuality import Individuality
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
logger = get_module_logger("reply_generator")
|
||||
logger = get_logger("reply_generator")
|
||||
|
||||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
@@ -92,12 +92,27 @@ class ReplyGenerator:
|
||||
max_tokens=300,
|
||||
request_type="reply_generation",
|
||||
)
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.personality_info = self._get_personality_prompt()
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.private_name = private_name
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
self.reply_checker = ReplyChecker(stream_id, private_name)
|
||||
|
||||
def _get_personality_prompt(self) -> str:
|
||||
"""获取个性提示信息"""
|
||||
prompt_personality = global_config.personality.personality
|
||||
|
||||
# 检查是否需要随机替换为状态
|
||||
if (
|
||||
global_config.personality.states
|
||||
and global_config.personality.state_probability > 0
|
||||
and random.random() < global_config.personality.state_probability
|
||||
):
|
||||
prompt_personality = random.choice(global_config.personality.states)
|
||||
|
||||
bot_name = global_config.BOT_NICKNAME
|
||||
return f"你的名字是{bot_name},你{prompt_personality};"
|
||||
|
||||
# 修改 generate 方法签名,增加 action_type 参数
|
||||
async def generate(
|
||||
self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str
|
||||
|
||||
@@ -3,7 +3,7 @@ from .chat_observer import ChatObserver
|
||||
from .conversation_info import ConversationInfo
|
||||
|
||||
# from src.individuality.individuality import Individuality # 不再需要
|
||||
from ...config.config import global_config
|
||||
from src.config.config import global_config
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class BrainChatting:
|
||||
# 循环控制内部状态
|
||||
self.running: bool = False
|
||||
self._loop_task: Optional[asyncio.Task] = None # 主循环任务
|
||||
self._new_message_event = asyncio.Event() # 新消息事件,用于打断 wait
|
||||
|
||||
# 添加循环信息管理相关的属性
|
||||
self.history_loop: List[CycleDetail] = []
|
||||
@@ -173,9 +174,10 @@ class BrainChatting:
|
||||
filter_intercept_message_level=1,
|
||||
)
|
||||
|
||||
# 如果有新消息,更新 last_read_time
|
||||
# 如果有新消息,更新 last_read_time 并触发事件以打断正在进行的 wait
|
||||
if len(recent_messages_list) >= 1:
|
||||
self.last_read_time = time.time()
|
||||
self._new_message_event.set() # 触发新消息事件,打断 wait
|
||||
|
||||
# 总是执行一次思考迭代(不管有没有新消息)
|
||||
# wait 动作会在其内部等待,不需要在这里处理
|
||||
@@ -434,6 +436,9 @@ class BrainChatting:
|
||||
last_check_time = self.last_read_time
|
||||
check_interval = 1.0 # 每秒检查一次
|
||||
|
||||
# 清除事件状态,准备等待新消息
|
||||
self._new_message_event.clear()
|
||||
|
||||
while self.running:
|
||||
# 检查是否有新消息
|
||||
recent_messages_list = message_api.get_messages_by_time_in_chat(
|
||||
@@ -453,8 +458,15 @@ class BrainChatting:
|
||||
logger.info(f"{self.log_prefix} 检测到新消息,恢复循环")
|
||||
return
|
||||
|
||||
# 等待一段时间后再次检查
|
||||
await asyncio.sleep(check_interval)
|
||||
# 等待新消息事件或超时后再次检查
|
||||
try:
|
||||
await asyncio.wait_for(self._new_message_event.wait(), timeout=check_interval)
|
||||
# 事件被触发,说明有新消息
|
||||
logger.info(f"{self.log_prefix} 检测到新消息事件,恢复循环")
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
# 超时后继续检查
|
||||
continue
|
||||
|
||||
async def _handle_action(
|
||||
self,
|
||||
@@ -674,7 +686,10 @@ class BrainChatting:
|
||||
logger.warning(f"{self.log_prefix} wait_seconds 参数格式错误,使用默认值 5 秒")
|
||||
wait_seconds = 5
|
||||
|
||||
logger.info(f"{self.log_prefix} 执行 wait 动作,等待 {wait_seconds} 秒")
|
||||
logger.info(f"{self.log_prefix} 执行 wait 动作,等待 {wait_seconds} 秒(可被新消息打断)")
|
||||
|
||||
# 清除事件状态,准备等待新消息
|
||||
self._new_message_event.clear()
|
||||
|
||||
# 记录动作信息
|
||||
await database_api.store_action_info(
|
||||
@@ -687,8 +702,17 @@ class BrainChatting:
|
||||
action_name="wait",
|
||||
)
|
||||
|
||||
# 等待指定时间
|
||||
await asyncio.sleep(wait_seconds)
|
||||
# 等待指定时间,但可被新消息打断
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._new_message_event.wait(),
|
||||
timeout=wait_seconds
|
||||
)
|
||||
# 如果事件被触发,说明有新消息到达
|
||||
logger.info(f"{self.log_prefix} wait 动作被新消息打断,提前结束等待")
|
||||
except asyncio.TimeoutError:
|
||||
# 超时正常完成
|
||||
pass
|
||||
|
||||
logger.info(f"{self.log_prefix} wait 动作完成,继续下一次思考")
|
||||
|
||||
@@ -707,7 +731,10 @@ class BrainChatting:
|
||||
# 使用默认等待时间
|
||||
wait_seconds = 3
|
||||
|
||||
logger.info(f"{self.log_prefix} 执行 listening(转换为 wait)动作,等待 {wait_seconds} 秒")
|
||||
logger.info(f"{self.log_prefix} 执行 listening(转换为 wait)动作,等待 {wait_seconds} 秒(可被新消息打断)")
|
||||
|
||||
# 清除事件状态,准备等待新消息
|
||||
self._new_message_event.clear()
|
||||
|
||||
# 记录动作信息
|
||||
await database_api.store_action_info(
|
||||
@@ -720,8 +747,17 @@ class BrainChatting:
|
||||
action_name="listening",
|
||||
)
|
||||
|
||||
# 等待指定时间
|
||||
await asyncio.sleep(wait_seconds)
|
||||
# 等待指定时间,但可被新消息打断
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._new_message_event.wait(),
|
||||
timeout=wait_seconds
|
||||
)
|
||||
# 如果事件被触发,说明有新消息到达
|
||||
logger.info(f"{self.log_prefix} listening 动作被新消息打断,提前结束等待")
|
||||
except asyncio.TimeoutError:
|
||||
# 超时正常完成
|
||||
pass
|
||||
|
||||
logger.info(f"{self.log_prefix} listening 动作完成,继续下一次思考")
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ class BrainPlanner:
|
||||
moderation_prompt=moderation_prompt_block,
|
||||
name_block=name_block,
|
||||
interest=interest,
|
||||
plan_style=global_config.personality.private_plan_style,
|
||||
plan_style=global_config.experimental.private_plan_style,
|
||||
)
|
||||
|
||||
return prompt, message_id_list
|
||||
|
||||
@@ -545,9 +545,9 @@ class HeartFChatting:
|
||||
new_message_count = message_api.count_new_messages(
|
||||
chat_id=self.chat_stream.stream_id, start_time=self.last_read_time, end_time=time.time()
|
||||
)
|
||||
need_reply = new_message_count >= random.randint(2, 3)
|
||||
need_reply = new_message_count >= random.randint(2, 3) or time.time() - self.last_read_time > 90
|
||||
if need_reply:
|
||||
logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复")
|
||||
logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复,或者上次回复时间超过90秒")
|
||||
|
||||
reply_text = ""
|
||||
first_replied = False
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import time
|
||||
import asyncio
|
||||
import urllib3
|
||||
|
||||
from abc import abstractmethod
|
||||
@@ -20,6 +21,9 @@ logger = get_logger("chat_message")
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# VLM 处理并发限制(避免同时处理太多图片导致卡死)
|
||||
_vlm_semaphore = asyncio.Semaphore(3)
|
||||
|
||||
# 这个类是消息数据类,用于存储和管理消息数据。
|
||||
# 它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。
|
||||
# 它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。
|
||||
@@ -73,20 +77,35 @@ class Message(MessageBase):
|
||||
str: 处理后的文本
|
||||
"""
|
||||
if segment.type == "seglist":
|
||||
# 处理消息段列表
|
||||
# 处理消息段列表 - 使用并行处理提升性能
|
||||
tasks = [self._process_message_segments(seg) for seg in segment.data] # type: ignore
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
segments_text = []
|
||||
for seg in segment.data:
|
||||
processed = await self._process_message_segments(seg) # type: ignore
|
||||
if processed:
|
||||
segments_text.append(processed)
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"处理消息段时出错: {result}")
|
||||
continue
|
||||
if result:
|
||||
segments_text.append(result)
|
||||
return " ".join(segments_text)
|
||||
elif segment.type == "forward":
|
||||
segments_text = []
|
||||
for node_dict in segment.data:
|
||||
# 处理转发消息 - 使用并行处理
|
||||
async def process_forward_node(node_dict):
|
||||
message = MessageBase.from_dict(node_dict) # type: ignore
|
||||
processed_text = await self._process_message_segments(message.message_segment)
|
||||
if processed_text:
|
||||
segments_text.append(f"{global_config.bot.nickname}: {processed_text}")
|
||||
return f"{global_config.bot.nickname}: {processed_text}"
|
||||
return None
|
||||
|
||||
tasks = [process_forward_node(node_dict) for node_dict in segment.data]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
segments_text = []
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"处理转发节点时出错: {result}")
|
||||
continue
|
||||
if result:
|
||||
segments_text.append(result)
|
||||
return "[合并消息]: " + "\n-- ".join(segments_text)
|
||||
else:
|
||||
# 处理单个消息段
|
||||
@@ -173,8 +192,9 @@ class MessageRecv(Message):
|
||||
self.is_picid = True
|
||||
self.is_emoji = False
|
||||
image_manager = get_image_manager()
|
||||
# print(f"segment.data: {segment.data}")
|
||||
_, processed_text = await image_manager.process_image(segment.data)
|
||||
# 使用 semaphore 限制 VLM 并发,避免同时处理太多图片
|
||||
async with _vlm_semaphore:
|
||||
_, processed_text = await image_manager.process_image(segment.data)
|
||||
return processed_text
|
||||
return "[发了一张图片,网卡了加载不出来]"
|
||||
elif segment.type == "emoji":
|
||||
@@ -183,7 +203,9 @@ class MessageRecv(Message):
|
||||
self.is_picid = False
|
||||
self.is_voice = False
|
||||
if isinstance(segment.data, str):
|
||||
return await get_image_manager().get_emoji_description(segment.data)
|
||||
# 使用 semaphore 限制 VLM 并发
|
||||
async with _vlm_semaphore:
|
||||
return await get_image_manager().get_emoji_description(segment.data)
|
||||
return "[发了一个表情包,网卡了加载不出来]"
|
||||
elif segment.type == "voice":
|
||||
self.is_picid = False
|
||||
|
||||
@@ -50,7 +50,15 @@ class Server:
|
||||
async def run(self):
|
||||
"""启动服务器"""
|
||||
# 禁用 uvicorn 默认日志和访问日志
|
||||
config = Config(app=self.app, host=self._host, port=self._port, log_config=None, access_log=False)
|
||||
# 设置 ws_max_size 为 100MB,支持大消息(如包含多张图片的转发消息)
|
||||
config = Config(
|
||||
app=self.app,
|
||||
host=self._host,
|
||||
port=self._port,
|
||||
log_config=None,
|
||||
access_log=False,
|
||||
ws_max_size=104_857_600, # 100MB
|
||||
)
|
||||
self._server = UvicornServer(config=config)
|
||||
try:
|
||||
await self._server.serve()
|
||||
|
||||
@@ -57,7 +57,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
|
||||
|
||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
||||
MMC_VERSION = "0.12.1"
|
||||
MMC_VERSION = "0.12.2"
|
||||
|
||||
|
||||
def get_key_comment(toml_table, key):
|
||||
|
||||
@@ -57,9 +57,6 @@ class PersonalityConfig(ConfigBase):
|
||||
visual_style: str = ""
|
||||
"""图片提示词"""
|
||||
|
||||
private_plan_style: str = ""
|
||||
"""私聊说话规则,行为风格"""
|
||||
|
||||
states: list[str] = field(default_factory=lambda: [])
|
||||
"""状态列表,用于随机替换personality"""
|
||||
|
||||
@@ -713,8 +710,8 @@ class DebugConfig(ConfigBase):
|
||||
class ExperimentalConfig(ConfigBase):
|
||||
"""实验功能配置类"""
|
||||
|
||||
enable_friend_chat: bool = False
|
||||
"""是否启用好友聊天"""
|
||||
private_plan_style: str = ""
|
||||
"""私聊说话规则,行为风格(实验性功能)"""
|
||||
|
||||
chat_prompts: list[str] = field(default_factory=lambda: [])
|
||||
"""
|
||||
@@ -904,6 +901,13 @@ class DreamConfig(ConfigBase):
|
||||
|
||||
return False
|
||||
|
||||
dream_visible: bool = False
|
||||
"""
|
||||
做梦结果是否存储到上下文
|
||||
- True: 将梦境发送给配置的用户后,也会存储到聊天上下文中,在后续对话中可见
|
||||
- False: 仅发送梦境但不存储,不在后续对话上下文中出现
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
"""验证配置值"""
|
||||
if self.interval_minutes < 1:
|
||||
|
||||
@@ -215,11 +215,12 @@ async def generate_dream_summary(
|
||||
f"platform={platform!r}, user_id={user_id!r}"
|
||||
)
|
||||
else:
|
||||
dream_visible = global_config.dream.dream_visible
|
||||
ok = await send_api.text_to_stream(
|
||||
dream_content,
|
||||
stream_id=stream_id,
|
||||
typing=False,
|
||||
storage_message=True,
|
||||
storage_message=dream_visible,
|
||||
)
|
||||
if ok:
|
||||
logger.info(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config, model_config
|
||||
@@ -108,7 +107,7 @@ def init_memory_retrieval_prompt():
|
||||
- 你可以对查询思路给出简短的思考:思考要简短,直接切入要点
|
||||
- 先思考当前信息是否足够回答问题
|
||||
- 如果信息不足,则需要使用tool查询信息,你必须给出使用什么工具进行查询
|
||||
- 如果当前已收集的信息足够或信息不足确定无法找到答案,你必须调用finish_search工具结束查询
|
||||
- 如果当前已收集的信息足够或信息不足确定无法找到答案,你必须调用found_answer工具结束查询
|
||||
""",
|
||||
name="memory_retrieval_react_prompt_head",
|
||||
)
|
||||
@@ -312,7 +311,7 @@ async def _react_agent_solve_question(
|
||||
|
||||
return None
|
||||
|
||||
# 正常迭代:使用head_prompt决定调用哪些工具(包含finish_search工具)
|
||||
# 正常迭代:使用head_prompt决定调用哪些工具(包含found_answer工具)
|
||||
tool_definitions = tool_registry.get_tool_definitions()
|
||||
# tool_names = [tool_def["name"] for tool_def in tool_definitions]
|
||||
# logger.debug(f"ReAct Agent 第 {iteration + 1} 次迭代,问题: {question}|可用工具: {', '.join(tool_names)} (共{len(tool_definitions)}个)")
|
||||
@@ -373,7 +372,7 @@ async def _react_agent_solve_question(
|
||||
logger.error(f"ReAct Agent LLM调用失败: {response}")
|
||||
break
|
||||
|
||||
# 注意:这里会检查finish_search工具调用,如果检测到finish_search工具,会根据found_answer参数决定返回答案或退出查询
|
||||
# 注意:这里会检查found_answer工具调用,如果检测到found_answer工具,会根据answer参数决定返回答案或退出查询
|
||||
|
||||
assistant_message: Optional[Message] = None
|
||||
if tool_calls:
|
||||
@@ -403,115 +402,146 @@ async def _react_agent_solve_question(
|
||||
|
||||
# 处理工具调用
|
||||
if not tool_calls:
|
||||
# 如果没有工具调用,检查响应文本中是否包含finish_search函数调用格式
|
||||
# 如果没有工具调用,检查响应文本中是否包含found_answer函数调用格式或JSON格式
|
||||
if response and response.strip():
|
||||
# 尝试从文本中解析finish_search函数调用
|
||||
def parse_finish_search_from_text(text: str):
|
||||
"""从文本中解析finish_search函数调用,返回(found_answer, answer)元组,如果未找到则返回(None, None)"""
|
||||
# 首先尝试解析JSON格式的found_answer
|
||||
def parse_json_found_answer(text: str):
|
||||
"""从文本中解析JSON格式的found_answer,返回(found_answer, answer)元组,如果未找到则返回(None, None)"""
|
||||
if not text:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
# 尝试提取JSON对象(可能包含在代码块中或直接是JSON)
|
||||
json_text = text.strip()
|
||||
|
||||
# 如果包含代码块标记,提取JSON部分
|
||||
if "```json" in json_text:
|
||||
start = json_text.find("```json") + 7
|
||||
end = json_text.find("```", start)
|
||||
if end != -1:
|
||||
json_text = json_text[start:end].strip()
|
||||
elif "```" in json_text:
|
||||
start = json_text.find("```") + 3
|
||||
end = json_text.find("```", start)
|
||||
if end != -1:
|
||||
json_text = json_text[start:end].strip()
|
||||
|
||||
# 尝试解析JSON
|
||||
data = json.loads(json_text)
|
||||
|
||||
# 检查是否包含found_answer字段
|
||||
if isinstance(data, dict) and "found_answer" in data:
|
||||
found_answer = bool(data.get("found_answer", False))
|
||||
answer = data.get("answer", "")
|
||||
return found_answer, answer
|
||||
except (json.JSONDecodeError, ValueError, TypeError):
|
||||
# 如果JSON解析失败,尝试在文本中查找JSON对象
|
||||
try:
|
||||
# 查找第一个 { 和最后一个 } 之间的内容(更健壮的JSON提取)
|
||||
first_brace = text.find('{')
|
||||
if first_brace != -1:
|
||||
# 从第一个 { 开始,找到匹配的 }
|
||||
brace_count = 0
|
||||
json_end = -1
|
||||
for i in range(first_brace, len(text)):
|
||||
if text[i] == '{':
|
||||
brace_count += 1
|
||||
elif text[i] == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
json_end = i + 1
|
||||
break
|
||||
|
||||
if json_end != -1:
|
||||
json_text = text[first_brace:json_end]
|
||||
data = json.loads(json_text)
|
||||
if isinstance(data, dict) and "found_answer" in data:
|
||||
found_answer = bool(data.get("found_answer", False))
|
||||
answer = data.get("answer", "")
|
||||
return found_answer, answer
|
||||
except (json.JSONDecodeError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return None, None
|
||||
|
||||
# 尝试从文本中解析found_answer函数调用
|
||||
def parse_found_answer_from_text(text: str):
|
||||
"""从文本中解析found_answer函数调用,返回answer字符串,如果未找到则返回None
|
||||
如果answer存在且非空,表示找到答案;如果answer为空或不存在,表示未找到答案"""
|
||||
if not text:
|
||||
return None
|
||||
|
||||
# 查找finish_search函数调用位置(不区分大小写)
|
||||
func_pattern = "finish_search"
|
||||
# 查找found_answer函数调用位置(不区分大小写)
|
||||
func_pattern = "found_answer"
|
||||
text_lower = text.lower()
|
||||
func_pos = text_lower.find(func_pattern)
|
||||
if func_pos == -1:
|
||||
return None, None
|
||||
|
||||
# 查找函数调用的开始和结束位置
|
||||
# 从func_pos开始向后查找左括号
|
||||
start_pos = text.find("(", func_pos)
|
||||
if start_pos == -1:
|
||||
return None, None
|
||||
|
||||
# 查找匹配的右括号(考虑嵌套)
|
||||
paren_count = 0
|
||||
end_pos = start_pos
|
||||
for i in range(start_pos, len(text)):
|
||||
if text[i] == "(":
|
||||
paren_count += 1
|
||||
elif text[i] == ")":
|
||||
paren_count -= 1
|
||||
if paren_count == 0:
|
||||
end_pos = i
|
||||
break
|
||||
else:
|
||||
# 没有找到匹配的右括号
|
||||
return None, None
|
||||
|
||||
# 提取函数参数部分
|
||||
params_text = text[start_pos + 1 : end_pos]
|
||||
|
||||
# 解析found_answer参数(布尔值,可能是true/false/True/False)
|
||||
found_answer = None
|
||||
found_answer_patterns = [
|
||||
r"found_answer\s*=\s*true",
|
||||
r"found_answer\s*=\s*True",
|
||||
r"found_answer\s*=\s*false",
|
||||
r"found_answer\s*=\s*False",
|
||||
]
|
||||
for pattern in found_answer_patterns:
|
||||
match = re.search(pattern, params_text, re.IGNORECASE)
|
||||
if match:
|
||||
found_answer = "true" in match.group(0).lower()
|
||||
break
|
||||
return None
|
||||
|
||||
# 解析answer参数(字符串,使用extract_quoted_content)
|
||||
answer = extract_quoted_content(text, "finish_search", "answer")
|
||||
answer = extract_quoted_content(text, "found_answer", "answer")
|
||||
|
||||
# 如果answer存在(即使是空字符串),也返回它(空字符串表示未找到答案)
|
||||
return answer
|
||||
|
||||
return found_answer, answer
|
||||
# 首先尝试解析JSON格式
|
||||
parsed_found_answer_json, parsed_answer_json = parse_json_found_answer(response)
|
||||
is_json_format = parsed_found_answer_json is not None
|
||||
|
||||
# 如果JSON解析成功,使用JSON结果
|
||||
if is_json_format:
|
||||
parsed_answer = parsed_answer_json
|
||||
has_answer = parsed_found_answer_json and parsed_answer and parsed_answer.strip()
|
||||
else:
|
||||
# 如果JSON解析失败,尝试解析函数调用格式
|
||||
parsed_answer = parse_found_answer_from_text(response)
|
||||
# 如果answer存在且非空,表示找到答案;否则表示未找到答案
|
||||
has_answer = parsed_answer is not None and parsed_answer.strip() != ""
|
||||
|
||||
parsed_found_answer, parsed_answer = parse_finish_search_from_text(response)
|
||||
|
||||
if parsed_found_answer is not None:
|
||||
# 检测到finish_search函数调用格式
|
||||
if parsed_found_answer:
|
||||
if parsed_answer is not None or is_json_format:
|
||||
# 检测到found_answer格式(可能是JSON格式或函数调用格式)
|
||||
format_type = "JSON格式" if is_json_format else "函数调用格式"
|
||||
if has_answer:
|
||||
# 找到了答案
|
||||
if parsed_answer:
|
||||
step["actions"].append(
|
||||
{
|
||||
"action_type": "finish_search",
|
||||
"action_params": {"found_answer": True, "answer": parsed_answer},
|
||||
}
|
||||
)
|
||||
step["observations"] = ["检测到finish_search文本格式调用,找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search文本格式找到关于问题{question}的答案: {parsed_answer}"
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status=f"找到答案:{parsed_answer}",
|
||||
)
|
||||
|
||||
return True, parsed_answer, thinking_steps, False
|
||||
else:
|
||||
# found_answer为True但没有提供answer,视为错误,继续迭代
|
||||
logger.warning(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 finish_search文本格式found_answer为True但未提供answer"
|
||||
)
|
||||
else:
|
||||
# 未找到答案,直接退出查询
|
||||
step["actions"].append(
|
||||
{"action_type": "finish_search", "action_params": {"found_answer": False}}
|
||||
{
|
||||
"action_type": "found_answer",
|
||||
"action_params": {"answer": parsed_answer},
|
||||
}
|
||||
)
|
||||
step["observations"] = ["检测到finish_search文本格式调用,未找到答案"]
|
||||
step["observations"] = [f"检测到found_answer{format_type}调用,找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search文本格式判断未找到答案"
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer{format_type}找到关于问题{question}的答案: {parsed_answer[:100]}..."
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status="未找到答案:通过finish_search文本格式判断未找到答案",
|
||||
final_status=f"找到答案:{parsed_answer}",
|
||||
)
|
||||
|
||||
return True, parsed_answer, thinking_steps, False
|
||||
else:
|
||||
# 未找到答案,直接退出查询
|
||||
step["actions"].append(
|
||||
{"action_type": "found_answer", "action_params": {"answer": ""}}
|
||||
)
|
||||
step["observations"] = [f"检测到found_answer{format_type}调用,未找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer{format_type}判断未找到答案"
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status="未找到答案:通过found_answer文本格式判断未找到答案",
|
||||
)
|
||||
|
||||
return False, "", thinking_steps, False
|
||||
|
||||
# 如果没有检测到finish_search格式,记录思考过程,继续下一轮迭代
|
||||
# 如果没有检测到found_answer格式,记录思考过程,继续下一轮迭代
|
||||
step["observations"] = [f"思考完成,但未调用工具。响应: {response}"]
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 思考完成但未调用工具: {response}"
|
||||
@@ -525,62 +555,55 @@ async def _react_agent_solve_question(
|
||||
continue
|
||||
|
||||
# 处理工具调用
|
||||
# 首先检查是否有finish_search工具调用,如果有则立即返回,不再处理其他工具
|
||||
finish_search_found = None
|
||||
finish_search_answer = None
|
||||
# 首先检查是否有found_answer工具调用,如果有则立即返回,不再处理其他工具
|
||||
found_answer_answer = None
|
||||
for tool_call in tool_calls:
|
||||
tool_name = tool_call.func_name
|
||||
tool_args = tool_call.args or {}
|
||||
|
||||
if tool_name == "finish_search":
|
||||
finish_search_found = tool_args.get("found_answer", False)
|
||||
finish_search_answer = tool_args.get("answer", "")
|
||||
if tool_name == "found_answer":
|
||||
found_answer_answer = tool_args.get("answer", "")
|
||||
|
||||
if finish_search_found:
|
||||
# 如果answer存在且非空,表示找到答案;否则表示未找到答案
|
||||
if found_answer_answer and found_answer_answer.strip():
|
||||
# 找到了答案
|
||||
if finish_search_answer:
|
||||
step["actions"].append(
|
||||
{
|
||||
"action_type": "finish_search",
|
||||
"action_params": {"found_answer": True, "answer": finish_search_answer},
|
||||
}
|
||||
)
|
||||
step["observations"] = ["检测到finish_search工具调用,找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search工具找到关于问题{question}的答案: {finish_search_answer}"
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status=f"找到答案:{finish_search_answer}",
|
||||
)
|
||||
|
||||
return True, finish_search_answer, thinking_steps, False
|
||||
else:
|
||||
# found_answer为True但没有提供answer,视为错误
|
||||
logger.warning(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 finish_search工具found_answer为True但未提供answer"
|
||||
)
|
||||
else:
|
||||
# 未找到答案,直接退出查询
|
||||
step["actions"].append({"action_type": "finish_search", "action_params": {"found_answer": False}})
|
||||
step["observations"] = ["检测到finish_search工具调用,未找到答案"]
|
||||
step["actions"].append(
|
||||
{
|
||||
"action_type": "found_answer",
|
||||
"action_params": {"answer": found_answer_answer},
|
||||
}
|
||||
)
|
||||
step["observations"] = ["检测到found_answer工具调用,找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search工具判断未找到答案"
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer工具找到关于问题{question}的答案: {found_answer_answer}"
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status="未找到答案:通过finish_search工具判断未找到答案",
|
||||
final_status=f"找到答案:{found_answer_answer}",
|
||||
)
|
||||
|
||||
return True, found_answer_answer, thinking_steps, False
|
||||
else:
|
||||
# 未找到答案,直接退出查询
|
||||
step["actions"].append({"action_type": "found_answer", "action_params": {"answer": ""}})
|
||||
step["observations"] = ["检测到found_answer工具调用,未找到答案"]
|
||||
thinking_steps.append(step)
|
||||
logger.info(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer工具判断未找到答案"
|
||||
)
|
||||
|
||||
_log_conversation_messages(
|
||||
conversation_messages,
|
||||
head_prompt=first_head_prompt,
|
||||
final_status="未找到答案:通过found_answer工具判断未找到答案",
|
||||
)
|
||||
|
||||
return False, "", thinking_steps, False
|
||||
|
||||
# 如果没有finish_search工具调用,继续处理其他工具
|
||||
# 如果没有found_answer工具调用,继续处理其他工具
|
||||
tool_tasks = []
|
||||
for i, tool_call in enumerate(tool_calls):
|
||||
tool_name = tool_call.func_name
|
||||
@@ -590,8 +613,8 @@ async def _react_agent_solve_question(
|
||||
f"{react_log_prefix}第 {iteration + 1} 次迭代 工具调用 {i + 1}/{len(tool_calls)}: {tool_name}({tool_args})"
|
||||
)
|
||||
|
||||
# 跳过finish_search工具调用(已经在上面处理过了)
|
||||
if tool_name == "finish_search":
|
||||
# 跳过found_answer工具调用(已经在上面处理过了)
|
||||
if tool_name == "found_answer":
|
||||
continue
|
||||
|
||||
# 记录最后一次使用的工具名称(用于判断是否需要额外迭代)
|
||||
|
||||
@@ -15,7 +15,7 @@ from .query_chat_history import register_tool as register_query_chat_history
|
||||
from .query_lpmm_knowledge import register_tool as register_lpmm_knowledge
|
||||
from .query_person_info import register_tool as register_query_person_info
|
||||
from .query_words import register_tool as register_query_words
|
||||
from .found_answer import register_tool as register_finish_search
|
||||
from .found_answer import register_tool as register_found_answer
|
||||
from src.config.config import global_config
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def init_all_tools():
|
||||
register_query_chat_history()
|
||||
register_query_person_info()
|
||||
register_query_words() # 注册query_words工具
|
||||
register_finish_search() # 注册finish_search工具
|
||||
register_found_answer() # 注册found_answer工具
|
||||
|
||||
if global_config.lpmm_knowledge.lpmm_mode == "agent":
|
||||
register_lpmm_knowledge()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
finish_search工具 - 用于在记忆检索过程中结束查询
|
||||
found_answer工具 - 用于在记忆检索过程中结束查询
|
||||
"""
|
||||
|
||||
from src.common.logger import get_logger
|
||||
@@ -8,17 +8,16 @@ from .tool_registry import register_memory_retrieval_tool
|
||||
logger = get_logger("memory_retrieval_tools")
|
||||
|
||||
|
||||
async def finish_search(found_answer: bool, answer: str = "") -> str:
|
||||
async def found_answer(answer: str = "") -> str:
|
||||
"""结束查询
|
||||
|
||||
Args:
|
||||
found_answer: 是否找到了答案
|
||||
answer: 如果找到了答案,提供答案内容;如果未找到,可以为空
|
||||
answer: 如果找到了答案,提供答案内容;如果未找到答案,可以为空或不提供此参数
|
||||
|
||||
Returns:
|
||||
str: 确认信息
|
||||
"""
|
||||
if found_answer:
|
||||
if answer and answer.strip():
|
||||
logger.info(f"找到答案: {answer}")
|
||||
return f"已确认找到答案: {answer}"
|
||||
else:
|
||||
@@ -27,23 +26,17 @@ async def finish_search(found_answer: bool, answer: str = "") -> str:
|
||||
|
||||
|
||||
def register_tool():
|
||||
"""注册finish_search工具"""
|
||||
"""注册found_answer工具"""
|
||||
register_memory_retrieval_tool(
|
||||
name="finish_search",
|
||||
description="当你决定结束查询时,调用此工具。如果找到了明确答案,设置found_answer为true并在answer中提供答案;如果未找到答案,设置found_answer为false。只有在检索到明确、具体的答案时才设置found_answer为true,不要编造信息。",
|
||||
name="found_answer",
|
||||
description="当你决定结束查询时,调用此工具。如果找到了明确答案,在answer参数中提供答案内容;如果未找到答案,可以不提供answer参数或提供空字符串。只有在检索到明确、具体的答案时才提供answer,不要编造信息。",
|
||||
parameters=[
|
||||
{
|
||||
"name": "found_answer",
|
||||
"type": "boolean",
|
||||
"description": "是否找到了答案",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"name": "answer",
|
||||
"type": "string",
|
||||
"description": "如果found_answer为true,提供找到的答案内容,必须基于已收集的信息,不要编造;如果found_answer为false,可以为空",
|
||||
"description": "如果找到了答案,提供找到的答案内容,必须基于已收集的信息,不要编造;如果未找到答案,可以不提供此参数或提供空字符串",
|
||||
"required": False,
|
||||
},
|
||||
],
|
||||
execute_func=finish_search,
|
||||
execute_func=found_answer,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[inner]
|
||||
version = "7.3.3"
|
||||
version = "7.3.5"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||
# 如果你想要修改配置文件,请递增version的值
|
||||
@@ -40,20 +40,11 @@ multiple_reply_style = [
|
||||
multiple_probability = 0.3
|
||||
|
||||
# 麦麦的说话规则和行为规则:
|
||||
plan_style = """
|
||||
1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
|
||||
2.如果相同的内容已经被执行,请不要重复执行
|
||||
3.你对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题
|
||||
4.请控制你的发言频率,不要太过频繁的发言
|
||||
5.如果有人对你感到厌烦,请减少回复
|
||||
6.如果有人在追问你,或者话题没有说完,请你继续回复"""
|
||||
|
||||
|
||||
# 麦麦私聊的说话规则,行为风格:
|
||||
private_plan_style = """
|
||||
1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
|
||||
2.如果相同的内容已经被执行,请不要重复执行
|
||||
3.某句话如果已经被回复过,不要重复回复"""
|
||||
plan_style = """1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
|
||||
2.如果相同的action已经被执行,请不要重复执行该action
|
||||
3.如果有人对你感到厌烦,请减少回复
|
||||
4.如果有人在追问你,或者话题没有说完,请你继续回复
|
||||
5.请分析哪些对话是和你说的,哪些是其他人之间的互动,不要误认为其他人之间的互动是和你说的"""
|
||||
|
||||
# 多重人格,根据概率随机选择人格
|
||||
states = [
|
||||
@@ -153,6 +144,7 @@ first_delay_seconds = 1800 # 程序启动后首次做梦前的延迟时间(秒
|
||||
# 为空字符串时不推送。
|
||||
dream_send = ""
|
||||
|
||||
dream_visible = false # 做梦结果是否存储到上下文,True: 将梦境发送给配置的用户后,也会存储到聊天上下文中,在后续对话中可见;False: 仅发送梦境但不存储,不在后续对话上下文中出现
|
||||
# 做梦时间段配置,格式:["HH:MM-HH:MM", ...]
|
||||
# 如果列表为空,则表示全天允许做梦。
|
||||
# 如果配置了时间段,则只有在这些时间段内才会实际执行做梦流程。
|
||||
@@ -298,6 +290,12 @@ trust_xff = false # 是否启用X-Forwarded-For代理解析(默认false)
|
||||
secure_cookie = false # 是否启用安全Cookie(仅通过HTTPS传输,默认false)
|
||||
|
||||
[experimental] #实验性功能
|
||||
# 麦麦私聊的说话规则,行为风格(实验性功能)
|
||||
private_plan_style = """
|
||||
1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
|
||||
2.如果相同的内容已经被执行,请不要重复执行
|
||||
3.某句话如果已经被回复过,不要重复回复"""
|
||||
|
||||
# 为指定聊天添加额外的prompt配置
|
||||
# 格式: ["platform:id:type:prompt内容", ...]
|
||||
# 示例:
|
||||
|
||||
1
webui/dist/assets/icons-8bdCaZgy.js
vendored
Normal file
1
webui/dist/assets/icons-8bdCaZgy.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webui/dist/assets/icons-9Z4kBNLK.js
vendored
1
webui/dist/assets/icons-9Z4kBNLK.js
vendored
File diff suppressed because one or more lines are too long
91
webui/dist/assets/index-DC6giT2e.js
vendored
91
webui/dist/assets/index-DC6giT2e.js
vendored
File diff suppressed because one or more lines are too long
94
webui/dist/assets/index-DD4VGX3W.js
vendored
Normal file
94
webui/dist/assets/index-DD4VGX3W.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webui/dist/assets/index-DseBW7cm.css
vendored
1
webui/dist/assets/index-DseBW7cm.css
vendored
File diff suppressed because one or more lines are too long
1
webui/dist/assets/index-RB5cYCSR.css
vendored
Normal file
1
webui/dist/assets/index-RB5cYCSR.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
webui/dist/fonts/JetBrainsMono-Medium.ttf
vendored
Normal file
BIN
webui/dist/fonts/JetBrainsMono-Medium.ttf
vendored
Normal file
Binary file not shown.
6
webui/dist/index.html
vendored
6
webui/dist/index.html
vendored
@@ -11,21 +11,21 @@
|
||||
<link rel="icon" type="image/x-icon" href="/maimai.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MaiBot Dashboard</title>
|
||||
<script type="module" crossorigin src="/assets/index-DC6giT2e.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DD4VGX3W.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-vendor-BmxF9s7Q.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-9vIXuQkh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-BqoaXoQ1.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-core-DyJi0yyw.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-extra-DmmnfeQE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-simvewUa.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-9Z4kBNLK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-8bdCaZgy.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/codemirror-TZqPU532.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/dnd-BiPfFtVp.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/misc-CJqnlRwD.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/reactflow-DtsZHOR4.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/uppy-DFP_VzYR.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/markdown-CKA5gBQ9.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DseBW7cm.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-RB5cYCSR.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="notranslate"></div>
|
||||
|
||||
Reference in New Issue
Block a user