conda create -n mindsearch python=3.10 -y
conda init
conda activate mindsearch
cd /workspaces/codespaces-blank
git clone https://github.com/InternLM/MindSearch.git && cd MindSearch && git checkout ae5b0c5
pip install -r requirements.txt # 这个requirements.txt需要修改,见附录
先启动后端
export SILICON_API_KEY=<API KEY>
conda activate mindsearch
cd /workspaces/codespaces-blank/MindSearch
python -m mindsearch.app --lang cn --model_format internlm_silicon --search_engine DuckDuckGoSearch --asy
再启动前端
conda activate mindsearch
cd /workspaces/codespaces-blank/MindSearch
python frontend/mindsearch_gradio.py
通过 duckduckgo 搜索:
国际象棋世界棋王赛的最新赛况
通过brave search API搜索(申请API,设置环境变量WEB_SEARCH_API_KEY):
export WEB_SEARCH_API_KEY=BSAU***
python -m mindsearch.app --lang cn --model_format internlm_silicon --search_engine BraveSearch --asy
俄罗斯大贝塔是什么梗
直接 duplicate 官方的space,即可。
我的个人账户的空间地址是
https://huggingface.co/spaces/hhhhhjjjjj/MindSearch
但是依然存在requirements.txt的问题需要修改,具体原因见附录。
需要duplicate 官方的space之后,修改requirements.txt,然后会自动重新构建space。
另外,对于某些比较复杂的问题,可能会调用过多查询,超过了搜索API接口的速率限制,导致报错。
一句话说,0.5.0rc2
版本的lagent调用搜索引擎API会报错,而且问题已经被后序PR解决。
所以 requirements.txt 指定安装了0.5.0rc2
版本的lagent会导致报错,我们要安装这个PR之后的lagent。
对于 requirements.txt:
- lagent==0.5.0rc2
# 使用主分支最新版本
+ lagent @ git+https://github.com/InternLM/lagent.git
# 或者甚至指定安装某个commit
+ lagent @ git+https://github.com/InternLM/lagent.git@bcc62ed
下面是具体的报错信息:
如果你使用的是 duckduckgo 的API,你会看到:
报错:API 方法名不匹配
AttributeError: 'AsyncDDGS' object has no attribute 'atext'. Did you mean: 'text'?
- 原因:代码使用了旧的 API 方法名
atext
,但新版本已经改成了text
- 本质:这是一个典型的 API breaking change 问题,库的接口发生变化导致旧代码不兼容
- 解决方法:在 lagent 库的源代码中,将
atext
改为text
如果你使用的是brave search 的API,你会看到:
TypeError: Invalid variable type: value should be str, int or float, got True of type <class 'bool'>
这是一个类型错误,出现在使用 lagent 库调用 Brave Search API 时。
问题原因:
- lagent 库在构建 API 请求时,直接使用了布尔值作为参数(
extra_snippets: True
) - 但底层的 yarl 库(用于构建 URL)只接受字符串、整数或浮点数作为参数值
- 这是一个依赖包兼容性问题
MindSearch采用多代理(Multi-Agent)框架,主要包含两个核心组件:
- 采用WebPlanner和WebSearcher两类代理协同工作
- WebPlanner负责高层规划,WebSearcher执行具体搜索
- 使用有向无环图(DAG)建模问题求解过程
- 支持子任务的并行执行
1.1 WebPlanner
- 功能:高级规划器,负责任务分解和推理编排
- 实现要点:
- 将复杂问题建模为有向无环图(DAG)结构
- 使用代码生成方式构建推理图
- 支持并行任务分发
- 通过图构建动态管理上下文
1.2 WebSearcher
- 功能:执行具体的网络搜索和信息提取
- 实现要点:
- 采用分层检索策略(Hierarchical Retrieval)
- 包含查询重写、内容聚合、页面选择等步骤
- 支持多搜索引擎API(Google, Bing, DuckDuckGo)
2.1 查询分解与规划
- WebPlanner接收用户查询
- 将查询分解为原子级子问题
- 构建DAG表示问题求解路径
- 生成Python代码实现图的动态构建
2.2 并行信息检索
- WebSearcher接收子问题
- 生成多个相似查询扩展搜索范围、查询重写
- 调用搜索API获取初步结果
- 基于URL合并搜索结果
- 选择最相关页面深入分析
2.3 上下文管理
- 在代理间显式传递上下文状态
- 使用图边关系处理上下文依赖
- 为每个WebSearcher添加父节点和根节点响应前缀
- 保持子任务焦点的同时维护整体上下文
- 分层检索是什么?
- 想象成"三重过滤"系统:
- 第一层(粗筛):快速浏览搜索结果的标题和简介,筛选出可能相关的页面
- 第二层(精筛):对筛选出的页面进行更详细的分析,确定真正相关的内容
- 第三层(深度分析):只对最相关的页面进行完整内容的下载和深入分析
举个例子:如果你搜索"苹果公司最新iPhone"
- 第一层可能返回100个结果
- 第二层筛选出20个相关度高的页面
- 第三层可能只分析5个最权威的源网页
- 基于URL合并是什么?
- 目的:去除重复内容,整合相似信息
- 工作方式:
例如搜索返回这些URL: - news.com/iphone15-review - news.com/iphone15-review?source=home - othernews.com/iphone15-review 系统会: 1. 识别出前两个URL指向同一内容 2. 只保留一个版本 3. 合并不同网站的补充信息
- 上下文管理是怎么工作的? 来看一个具体例子:
在这个例子中:
-
显式传递上下文:
原始问题: "分析iPhone 15的优缺点" 子任务A: "总体评价" + 原始问题 子任务B: "优点分析" + 原始问题 子任务C: "缺点分析" + 原始问题
每个子任务都知道自己在解决什么大问题
-
图边关系处理:
- B1(性能提升)知道自己是B(优点分析)的一部分
- C1(价格)知道自己是C(缺点分析)的一部分
-
父节点和根节点响应:
当C2(续航)执行时,它知道: - 根节点是"iPhone 15分析" - 父节点是"缺点分析" - 自己的任务是"分析续航问题"
-
保持焦点同时维护整体:
- 每个小任务专注于自己的部分(如"分析续航")
- 但回答时会考虑整体上下文(这是在分析iPhone 15的缺点)
- 避免答非所问或失去重点
这种设计让系统能够:
- 准确理解每个子任务的目标
- 保持回答的连贯性
- 避免重复工作
- 产生结构化的完整答案
这样的设计类似于人类写研究报告:
- 知道整体是什么(报告主题)
- 明确自己负责哪部分(具体章节)
- 理解和其他部分的关系(章节关联)
- 既能专注细节又不丢失整体视角
简单分析 /MindSearch/mindsearch/app.py
的逻辑。
主要组件:
- FastAPI作为Web框架
- EventSourceResponse用于流式响应
- janus.Queue用于异步/同步队列转换
- 支持CORS跨域请求
- 输入阶段 当你输入一个问题,比如"分析iPhone 15的优缺点":
# 通过 /solve 接口接收你的问题
@app.post('/solve')
async def run(request: GenerationParams):
inputs = request.inputs # 这里接收到你的问题
# 初始化核心处理引擎
agent = init_agent(lang=args.lang,
model_format=args.model_format,
search_engine=args.search_engine)
- 处理阶段
# 开始处理你的问题
for response in agent.stream_chat(inputs):
# agent.stream_chat 是核心处理过程,它会:
# 1. 分解你的问题(比如拆分为"性能"、"相机"、"电池"等子问题)
# 2. 对每个子问题进行网络搜索
# 3. 分析搜索结果
# 4. 整合信息形成答案
- 实时反馈 系统会实时返回处理进度:
# 每当有新的进展,就会通过这个机制返回给你
response_json = json.dumps({
'response': agent_return, # 当前的处理结果
'current_node': node_name # 正在处理哪个部分
})
yield {'data': response_json}
MindSearch的核心魔法在 agent.stream_chat(inputs)
内部。
真正的核心在 init_agent()
创建的 agent 对象中,这才是 MindSearch 的"大脑":
MindSearch的核心流程是:
- WebPlanner大脑
# 比如你问:"分析iPhone 15的优缺点"
WebPlanner会思考:
- "需要查什么?" -> 拆解成多个方面
- "如何查?" -> 设计搜索策略
- "查完后怎么整合?" -> 规划结果处理
- WebSearcher执行器
# WebPlanner规划后,WebSearcher负责:
- 并行执行多个搜索任务
- 对每个搜索结果进行筛选
- 提取有价值的信息
- 信息整合
# 所有搜索完成后:
- 整合各个方面的信息
- 去除重复内容
- 组织成连贯的答案
/MindSearch/mindsearch/app.py
这个文件是一个"调度员":
- 它负责接收用户问题
- 启动处理流程
- 实时反馈结果
- 但真正的"智慧"在 Agent 内部
如果想了解 MindSearch 最核心的部分,我们应该看 Agent 的代码。
文件在这里:
(mindsearch) root@intern-studio-50007388:~/mindsearch/MindSearch/mindsearch/agent# ls
init.py mindsearch_agent.py mindsearch_prompt.py models.py
这些就是核心代码文件,解释一下每个文件的作用:
- mindsearch_agent.py - 这是最核心的实现文件
这里包含了:
- WebPlanner的实现:如何分析问题、规划搜索
- WebSearcher的实现:如何执行搜索、处理结果
- 整个Agent的协调逻辑
- mindsearch_prompt.py - 整个过程用到的全部LLM提示词
- models.py - 是LLM的接口规范
- init.py - 这是包的初始化文件,负责导出主要的类和函数和配置初始化
MindSearch的核心工作机制:
从代码来看,MindSearch是由两个主要组件构成的:
- WebPlanner (大脑)
class MindSearchAgent(BaseAgent):
def stream_chat(self, message, **kwargs):
# 1. 接收用户问题
# 2. 构建搜索图(把大问题分解成小问题)
# 3. 协调多个WebSearcher执行搜索
# 4. 整合结果形成最终答案
- WebSearcher (搜索执行器)
class SearcherAgent(Internlm2Agent):
def stream_chat(self, question, root_question=None, parent_response=None):
# 1. 执行具体的搜索任务
# 2. 分析搜索结果
# 3. 返回答案给Planner
工作流程是这样的:
- 问题接收:
def init_agent(lang='cn', model_format='internlm_server',search_engine='DuckDuckGoSearch'):
# 初始化大模型和搜索引擎
# 设置提示词模板(中文/英文)
# 配置搜索参数
- 问题分解(使用 WebSearchGraph):
class WebSearchGraph:
def add_root_node(self, node_content):
# 添加主问题
def add_node(self, node_name, node_content):
# 添加子问题节点
# 启动搜索线程处理子问题
- 并行搜索:
# 在WebSearchGraph中
def model_stream_thread():
agent = SearcherAgent(**self.searcher_cfg)
for answer in agent.stream_chat(
node_content, # 子问题
self.nodes['root']['content'], # 原始问题
parent_response=parent_response): # 前序节点的答案
# 返回搜索结果
- 结果整合:
# 在MindSearchAgent中
def _generate_reference(self, agent_return, code, as_dict):
# 收集所有搜索结果
# 整理引用信息
# 生成最终答案
以一个实际例子来说明:
假设用户问:"分析iPhone 15的优缺点"
- WebPlanner会:
# 1. 创建搜索图
graph.add_root_node("分析iPhone 15的优缺点")
# 2. 分解子问题
graph.add_node("性能分析", "iPhone 15的性能参数和实测表现如何?")
graph.add_node("相机评测", "iPhone 15的相机系统有什么特点?")
graph.add_node("续航分析", "iPhone 15的电池续航表现如何?")
- WebSearcher会:
- 并行搜索每个子问题
- 分析搜索结果
- 返回有价值的信息
- 最后:
- 整合所有搜索结果
- 生成结构化的完整答案
- 附带信息来源引用
这就是为什么MindSearch能快速处理复杂问题 - 它通过多代理并行处理,像团队合作一样高效工作。
整理 mindsearch_prompt.py 中的提示词。这些提示词可以分为三大核心部分:
- 搜索者角色提示词 (Searcher System Prompt)
# 中英文两个版本,主要包含:
searcher_system_prompt_cn/en = """
A. 角色定义
- 你是一个可以调用网络搜索工具的智能助手
B. 工具说明
- 可用工具列表:{tool_info}
C. 回复格式
- 调用工具的格式:
<|action_start|><|plugin|>{"name": "tool_name", "parameters": {"param1": "value1"}}<|action_end|>
D. 具体要求
1. 每个关键点需标注引用来源 [[1]][[2]]
2. 回答要基于搜索结果,详细完备
"""
- 图构建提示词 (Graph Prompt)
GRAPH_PROMPT_CN/EN = """
A. API文档
- WebSearchGraph类的详细说明
- 所有可用方法的参数和返回值
B. 任务说明
- 如何将问题拆分成子问题
- 如何构建搜索图
- 如何步骤化处理问题
C. 注意事项
1. 每个搜索节点必须是单一问题
2. 不要杜撰搜索结果
3. 避免重复提问
4. response节点单独添加
5. 代码块规范
"""
- 最终回答提示词 (Final Response Prompt)
FINAL_RESPONSE_CN/EN = """
A. 格式要求
- 标注引用来源 [[id]]
- 不使用模糊表达
B. 内容规范
- 逻辑清晰,层次分明
- 内容全面且完备
- 不包含原始问答对
C. 风格指导
- 专业严谨
- 避免口语化
- 保持一致性
"""
- 模板和示例
# 输入模板
searcher_input_template = """
## 主问题/Final Problem
{topic}
## 当前问题/Current Problem
{question}
"""
# 上下文模板
searcher_context_template = """
## 历史问题/Historical Problem
{question}
回答/Answer:{answer}
"""
# 搜索示例
fewshot_example = """
包含具体的搜索和选择示例:
1. 如何搜索
2. 如何选择结果
"""
这些提示词共同构成了一个完整的指导系统:
- 搜索者提示词:指导如何搜索和处理信息
- 图构建提示词:指导如何分解问题和构建搜索路径
- 最终回答提示词:确保输出高质量的答案
每种提示词都有中英文版本,并且包含详细的格式要求和示例。
让我通过分析这两个核心类来解释MindSearch的工作原理:
- MindSearchAgent (总指挥官)
class MindSearchAgent(BaseAgent):
def __init__(self, llm, searcher_cfg, protocol=MindSearchProtocol(), max_turn=10):
self.local_dict = {} # 存储本地变量
self.llm = llm # 大语言模型
self.max_turn = max_turn # 最大对话轮次
WebSearchGraph.searcher_cfg = searcher_cfg # 搜索配置
核心方法是 stream_chat
,工作流程:
def stream_chat(self, message, **kwargs):
# 1. 初始化会话
inner_history = message[:]
agent_return = AgentReturn()
# 2. 主循环(最多max_turn轮)
for _ in range(self.max_turn):
# 2.1 生成提示词
prompt = self._protocol.format(inner_step=inner_history)
# 2.2 获取LLM响应
for model_state, response, _ in self.llm.stream_chat(prompt):
# 处理响应
_, language, action = self._protocol.parse(response)
code = action['parameters']['command'] if action else ''
# 2.3 如果有代码要执行
if code:
# 执行代码(通常是构建和更新搜索图)
yield from self._process_code(agent_return, inner_history,
code, as_dict, return_early)
else:
# 如果没有代码,说明回答完成
agent_return.state = AgentStatusCode.END
yield deepcopy(agent_return)
return
- SearcherAgent (搜索执行者)
class SearcherAgent(Internlm2Agent):
def __init__(self, template='{query}', **kwargs):
super().__init__(**kwargs)
self.template = template
def stream_chat(self, question, root_question=None, parent_response=None):
# 1. 构建搜索查询
message = self.template['input'].format(
question=question,
topic=root_question
)
# 2. 如果有父节点的响应,添加上下文
if parent_response:
if 'context' in self.template:
parent_response = [
self.template['context'].format(**item)
for item in parent_response
]
message = '\n'.join(parent_response + [message])
# 3. 执行搜索并处理结果
for agent_return in super().stream_chat(message):
agent_return.type = 'searcher'
agent_return.content = question
yield deepcopy(agent_return)
-
关键机制说明
-
问题处理机制
- MindSearchAgent接收问题
- 使用LLM分析和分解问题
- 通过代码构建搜索图
- 并行搜索机制
- 每个子问题创建一个SearcherAgent
- 多个SearcherAgent并行工作
- 通过线程池管理搜索任务
- 结果整合机制
- 使用队列收集搜索结果
- 维护搜索图的状态
- 最终生成完整答案
- 状态管理
- 跟踪每个节点的状态
- 管理搜索任务的生命周期
- 确保结果的完整性
这就是MindSearch的核心工作方式:它像一个指挥官(MindSearchAgent)带领多个搜索专家(SearcherAgent),共同完成信息搜索和整合的任务。每个组件都有明确的职责,通过精心的协调实现高效的并行处理。