多智能体协作框架Shogun:基于中心化架构的LLM智能体编排实践
1. 项目概述与核心价值
最近在探索多智能体系统(Multi-Agent System, MAS)的落地应用时,我偶然发现了一个名为yohey-w/multi-agent-shogun的开源项目。这个名字本身就很有意思,“Shogun”在日语里是“将军”的意思,暗示着这个框架旨在扮演一个指挥、协调多个智能体(Agent)的“统帅”角色。这立刻引起了我的兴趣,因为在当前大语言模型(LLM)驱动的智能体浪潮中,如何高效、稳定地编排多个具备不同能力的智能体协同工作,解决复杂任务,是一个极具挑战性且实际价值巨大的课题。
简单来说,multi-agent-shogun是一个基于 Python 的、轻量级的多智能体协作框架。它不像一些重型框架那样试图包办一切,而是聚焦于解决多智能体协作中最核心的几个痛点:智能体间的通信、任务编排与路由、以及协作流程的清晰定义。如果你正在尝试将多个 LLM 智能体(比如一个负责数据分析、一个负责代码生成、一个负责报告撰写)串联起来,构建一个自动化的工作流,或者想研究智能体间的对话与协作机制,那么这个项目提供了一个非常干净、易于上手的起点。
它的核心价值在于“清晰”和“可控”。通过定义明确的角色(Role)、消息(Message)和会话(Session)机制,它让开发者能够像搭积木一样,将不同的智能体能力组合起来,并清晰地观察到它们之间的交互过程。这对于调试复杂的多智能体交互逻辑至关重要。接下来,我将深入拆解这个项目的设计思路、核心组件,并分享如何基于它构建一个实际可用的多智能体应用。
2. 核心架构与设计哲学拆解
2.1 为什么是“Shogun”?—— 中心化协调的利与弊
多智能体系统的架构大致可以分为两类:去中心化(Peer-to-Peer)和中心化(Centralized)。multi-agent-shogun明显采用了后者的思想,这也是其命名为“将军”的缘由。
在去中心化架构中,每个智能体都相对独立,它们通过发布/订阅消息或直接相互调用进行通信。这种模式灵活性高,但复杂度也高,尤其是在需要保证任务执行顺序、处理智能体间依赖或避免循环调用时,会变得难以管理和调试。
Shogun框架则引入了一个核心的协调者角色——你可以理解为“将军”。这个协调者(通常是框架的Session或一个专门的Orchestrator Agent)负责接收总任务,将其分解为子任务,并根据预定义的规则或策略,将子任务分派给最合适的“士兵”(即功能智能体)。智能体之间不直接对话,而是通过“将军”中转。
这种设计的优势非常明显:
- 控制流清晰:整个系统的执行流程一目了然,从“将军”的视角可以完整地追踪任务从下发到完成的全部路径。
- 易于编排:复杂任务的分解、串行/并行执行、条件分支等逻辑,都可以在协调者层面集中定义,比在分散的智能体间维护这些逻辑要简单得多。
- 简化通信:智能体只需实现一个标准的“接收指令-返回结果”接口,无需关心消息该发给谁,降低了智能体本身的复杂度。
- 便于监控与日志:所有交互都经过中心节点,自然形成了一个完整的审计日志,对于问题排查和系统优化至关重要。
当然,中心化架构的潜在瓶颈在于协调者本身。如果协调逻辑过于复杂或成为性能瓶颈,会影响整个系统的扩展性。Shogun框架的轻量级特性,意味着它更适合于任务逻辑明确、智能体数量适中(例如几个到几十个)的场景,比如自动化工作流、客服路由、代码审查流水线等,而不是需要海量智能体动态博弈的模拟环境。
2.2 核心组件深度解析
要理解Shogun,必须吃透它的几个核心抽象。这些抽象构成了整个框架的骨架。
Agent(智能体):这是执行具体任务的基本单元。每个Agent都需要定义其name(唯一标识)和role(角色描述,用于任务路由)。最关键的是要实现act方法。这个方法接收一个Message对象(包含了任务指令和上下文),并返回一个新的Message对象(包含执行结果)。Agent的内部实现可以是调用一个本地函数、查询数据库、或者最常用的——调用一个 LLM(如 OpenAI GPT、 Anthropic Claude 或本地部署的模型)。框架本身不绑定任何特定的 LLM SDK,这给了开发者最大的灵活性。
# 一个简单的计算器智能体示例 class CalculatorAgent(Agent): def __init__(self): super().__init__(name="calculator", role="Performs arithmetic calculations.") def act(self, message: Message) -> Message: # 从消息内容中解析出计算表达式,例如 “calculate 3 + 5” try: # 这里简化处理,实际可能需要更复杂的自然语言解析 expression = message.content.replace("calculate", "").strip() result = eval(expression) # 注意:生产环境慎用eval,此处仅为示例 return Message(sender=self.name, content=f"The result is {result}") except Exception as e: return Message(sender=self.name, content=f"Calculation error: {e}")Message(消息):智能体之间通信的唯一载体。它不仅仅是文本内容(content),还包含了元数据,如发送者(sender)、接收者(receiver,在中心化架构中可能由协调者决定)、以及可选的metadata字典用于携带任意自定义数据(如工具调用结果、中间状态等)。设计良好的Message结构是保证智能体有效协作的基础。Shogun的消息模型鼓励将对话历史、工具执行结果等结构化信息放入metadata,使content保持清晰的任务指令或回复。
Session(会话):这是“将军”角色的主要体现。一个Session管理了一次完整的多智能体协作过程。它维护着本次会话中所有智能体的注册表(Agent Registry),并持有一个消息列表(Message History),记录了从开始到结束的所有交互。Session的核心方法是run或process,它接收一个初始任务消息,然后根据内置的或自定义的Orchestration Logic(编排逻辑)来决定:
- 当前应该由哪个智能体来响应?
- 如何将当前对话上下文(历史消息)传递给该智能体?
- 如何处理该智能体的返回结果?是返回给用户,还是继续触发下一个智能体?
Orchestrator(编排器):这是Session的大脑,定义了具体的任务路由和流程控制策略。最简单的编排器是RoundRobinOrchestrator(轮询),或者RoleBasedOrchestrator(根据消息内容中的关键词匹配智能体的role描述)。对于复杂场景,你需要实现自定义的Orchestrator,它可能包含一个“规划智能体”(Planner Agent),先用一个 LLM 分析总任务,生成一个包含多个步骤的计划,然后Session再按计划依次调用相应的智能体执行。
注意:
Agent的role描述至关重要。一个好的role描述应该是具体、包含关键词的,例如“一个专门将自然语言指令转换为SQL查询的专家”就比“处理数据库”要好得多。这能极大地提高基于角色路由的编排器的准确性。
2.3 通信模式与协作流程
在Shogun的架构下,一次典型的协作流程如下:
- 初始化:创建
Session,并向其中注册所有需要的Agent(如WriterAgent,CoderAgent,ReviewerAgent)。 - 任务输入:用户或外部系统向
Session提交一个初始Message,内容为总任务,例如“开发一个Python函数,计算斐波那契数列,并生成使用说明文档”。 - 编排决策:
Session内部的Orchestrator被激活。它分析当前消息(初始消息)和会话历史(目前为空)。在一个高级实现中,Orchestrator可能首先调用一个PlannerAgent,该智能体分析任务后输出一个计划:“第一步,让CoderAgent编写函数;第二步,让ReviewerAgent检查代码;第三步,让WriterAgent撰写文档。” - 任务分派:
Orchestrator根据计划,选择第一个任务“编写函数”,并从注册表中选出最适合的CoderAgent,将任务指令(可能附加上下文)封装成Message发送给它。 - 智能体执行:
CoderAgent的act方法被调用。它可能会调用 LLM,生成一段 Python 代码,然后将代码作为Message.content返回,同时可能将格式化后的代码块放入metadata中。 - 结果处理与迭代:
Session收到CoderAgent的返回消息,将其加入会话历史。然后,Orchestrator再次被触发,查看当前状态(计划完成第一步)。接着,它选择ReviewerAgent,并将CoderAgent的输出作为上下文传递给ReviewerAgent,要求其进行代码审查。 - 循环直至完成:上述过程循环,直到
Orchestrator判断最终结果已产生(例如WriterAgent输出了完整的文档),或者达到某种终止条件(如最大轮次限制)。 - 最终输出:
Session将最终智能体产生的消息返回给用户。
整个过程中,所有Message都被存储在Session的历史记录中,形成了一个完整的、可追溯的决策链。这对于调试和优化智能体协作逻辑是无价的。
3. 从零构建一个多智能体协作实例
理论讲得再多,不如动手实践。让我们来构建一个简单的“技术方案咨询”多智能体系统。这个系统包含三个智能体:一个ArchitectAgent(架构师)负责给出技术选型建议,一个CoderAgent(程序员)负责提供示例代码片段,一个CriticAgent(评审员)负责指出前两者建议中的潜在问题。
3.1 环境准备与框架安装
首先,确保你的 Python 环境在 3.8 以上。然后安装multi-agent-shogun。由于它是一个较新的项目,建议直接从 GitHub 仓库安装最新版本。
pip install git+https://github.com/yohey-w/multi-agent-shogun.git # 或者,如果你克隆了仓库 # pip install -e /path/to/multi-agent-shogun接下来,我们需要设置 LLM。假设我们使用 OpenAI 的模型,你需要安装openai库并设置 API 密钥。
pip install openai export OPENAI_API_KEY='your-api-key-here' # 在Linux/macOS上 # 或者在代码中设置 os.environ['OPENAI_API_KEY'] = 'your-key'3.2 定义三个功能智能体
我们将创建三个智能体,它们都将通过调用 OpenAI API 来完成工作。
import os from openai import OpenAI from multi_agent_shogun.agent import Agent from multi_agent_shogun.message import Message client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) class ArchitectAgent(Agent): def __init__(self): # 角色描述要详细,包含关键词,便于路由 super().__init__( name="tech_architect", role="A senior technical architect. Expert in recommending technology stacks, system design patterns, and high-level solution outlines for given software requirements. Focus on scalability, maintainability, and best practices." ) def act(self, message: Message) -> Message: prompt = f""" As a senior technical architect, your task is to provide a high-level technology recommendation. User Requirement: {message.content} Please provide: 1. Recommended technology stack (e.g., backend framework, database, frontend). 2. Key architectural considerations. 3. A brief rationale for your choices. Keep the response concise and structured. """ try: response = client.chat.completions.create( model="gpt-4o-mini", # 或 gpt-4-turbo messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=500 ) content = response.choices[0].message.content return Message(sender=self.name, content=content, metadata={"type": "architecture_advice"}) except Exception as e: return Message(sender=self.name, content=f"Architect Agent Error: {e}") class CoderAgent(Agent): def __init__(self): super().__init__( name="code_generator", role="A pragmatic software developer. Generates clean, functional code snippets in various programming languages based on requirements and architectural context. Prefers practical examples over theoretical explanations." ) def act(self, message: Message) -> Message: # 消息的 metadata 可能包含架构建议,我们将其作为上下文 arch_context = message.metadata.get('architecture_advice', 'No architecture context provided.') prompt = f""" As a software developer, generate a practical code snippet based on the requirement and architectural context. User Requirement: {message.content} Architectural Context (if any): {arch_context} Please provide: 1. A single, focused code snippet (in the most appropriate language) that addresses the core of the requirement. 2. Brief comments explaining key parts of the code. 3. Do not write extensive documentation or multiple examples. One good snippet is enough. If the requirement is too vague or non-coding related, state that clearly. """ try: response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.5, # 代码生成温度低一些,更稳定 max_tokens=600 ) content = response.choices[0].message.content return Message(sender=self.name, content=content, metadata={"type": "code_snippet"}) except Exception as e: return Message(sender=self.name, content=f"Coder Agent Error: {e}") class CriticAgent(Agent): def __init__(self): super().__init__( name="devil_advocate", role="A critical reviewer. Identifies potential pitfalls, oversights, security issues, or unrealistic aspects in technical proposals and code. Focuses on robustness, edge cases, and practical deployment concerns." ) def act(self, message: Message) -> Message: # 这个智能体需要看到之前所有智能体的输出作为上下文 # 在Shogun中,Session会传递完整的会话历史,但act方法只接收上一个消息。 # 因此,我们需要在Orchestrator或Session层面确保Critic能拿到足够的历史信息。 # 这里我们假设message.content已经包含了之前对话的总结,或者我们通过metadata传递。 prompt = f""" As a critical reviewer, analyze the following technical proposal and code for potential issues. Full Proposal and Code Context: {message.content} Please list: 1. Top 3 potential technical risks or oversights. 2. Any security concerns (if applicable). 3. Suggestions for improvement or clarification needed. Be concise and direct. """ try: response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.3, # 批评性分析需要更低的随机性 max_tokens=400 ) content = response.choices[0].message.content return Message(sender=self.name, content=content, metadata={"type": "critical_review"}) except Exception as e: return Message(sender=self.name, content=f"Critic Agent Error: {e}")3.3 实现一个简单的顺序编排器
Shogun可能提供了基础的编排器,但为了演示,我们实现一个自定义的SequentialOrchestrator,它简单地按预定义顺序调用智能体。
from typing import List from multi_agent_shogun.orchestrator import Orchestrator from multi_agent_shogun.session import Session class SequentialOrchestrator(Orchestrator): def __init__(self, agent_order: List[str]): """ agent_order: 智能体名称的执行顺序列表,例如 ['tech_architect', 'code_generator', 'devil_advocate'] """ self.agent_order = agent_order self.current_step = 0 def select_agent(self, session: Session) -> str: """根据当前步骤选择智能体""" if self.current_step < len(self.agent_order): selected_agent_name = self.agent_order[self.current_step] self.current_step += 1 return selected_agent_name else: return None # 表示会话结束 def should_continue(self, session: Session) -> bool: """判断是否继续执行下一个智能体""" return self.current_step < len(self.agent_order)3.4 组装并运行会话
现在,我们把所有部件组装起来,形成一个完整的Session。
def main(): # 1. 创建智能体实例 architect = ArchitectAgent() coder = CoderAgent() critic = CriticAgent() # 2. 创建编排器,定义执行顺序:架构师 -> 程序员 -> 评审员 orchestrator = SequentialOrchestrator(['tech_architect', 'code_generator', 'devil_advocate']) # 3. 创建会话,并注册智能体 session = Session(orchestrator=orchestrator) session.register_agent(architect) session.register_agent(coder) session.register_agent(critic) # 4. 定义用户需求 user_requirement = "I need to build a REST API endpoint that accepts a user ID and returns the user's profile information along with their last 5 orders. Please advise." # 5. 创建初始消息 initial_message = Message(sender="user", content=user_requirement) print("=== Starting Multi-Agent Consultation ===") print(f"User Requirement: {user_requirement}\n") # 6. 运行会话 final_message = session.run(initial_message) # 7. 打印完整的会话历史 print("\n=== Full Session History ===") for i, msg in enumerate(session.message_history): print(f"\n--- Turn {i+1}: {msg.sender} ---") print(msg.content[:300] + "..." if len(msg.content) > 300 else msg.content) # 打印前300字符 print("\n=== Final Output ===") print(final_message.content if final_message else "Session did not produce a final message.") if __name__ == "__main__": main()运行这段代码,你会看到三个智能体依次被调用。ArchitectAgent会给出技术栈建议(比如使用 FastAPI + SQLAlchemy + PostgreSQL),CoderAgent会根据这个建议生成一个示例的 FastAPI 端点代码,最后CriticAgent会对前面的建议和代码提出批评意见(比如缺少错误处理、N+1查询问题等)。
实操心得:在实际运行中,你可能会发现
CriticAgent的批评不够深入,因为它只看到了前一个CoderAgent的输出,而看不到更早的ArchitectAgent的输出。为了解决这个问题,我们需要改进编排逻辑,让CriticAgent能获得更完整的上下文。这可以通过在Orchestrator向CriticAgent发送消息前,将之前所有智能体的输出整合到一个Message中来实现。这揭示了多智能体系统设计中的一个关键点:上下文的管理与传递策略,需要根据协作模式精心设计。
4. 高级技巧与生产环境考量
一个能在玩具示例中运行的系统,距离生产可用还有很长的路。以下是基于Shogun框架构建稳健的多智能体应用时,必须考虑的几点。
4.1 智能体设计的健壮性
你的智能体act方法是系统的核心,必须非常健壮。
错误处理与降级:LLM 调用可能失败(网络、限流、上下文过长)。智能体的act方法必须有完善的try-except块,并返回一个明确的错误消息Message,而不是抛出异常导致整个会话崩溃。更好的做法是实现重试机制(使用指数退避)和降级策略(例如,调用一个更便宜、更稳定的模型)。
上下文窗口管理:LLM 有上下文长度限制。当Session的历史消息越来越长时,直接将其全部塞给下一个智能体是不行的。你需要实现一个ContextManager,其职责是:
- 摘要:将冗长的历史对话总结成一段精炼的上下文。
- 选择性注入:只选取与当前智能体任务最相关的历史消息进行传递。
- 工具调用结果处理:如果智能体使用了工具(如网络搜索、代码执行),工具返回的结果可能很长,需要被压缩或提取关键信息后再放入上下文。
提示词工程:智能体的能力很大程度上取决于提示词。为每个智能体设计清晰、具体、带有约束条件的提示词模板(就像我们上面做的那样)。使用少样本示例(Few-shot Examples)可以显著提高输出的稳定性和格式一致性。将提示词模板外部化(如放在 YAML 或 JSON 文件中)便于管理和 A/B 测试。
4.2 编排逻辑的复杂性管理
简单的顺序执行很快会不够用。你需要处理更复杂的流程。
条件分支与循环:根据某个智能体的输出内容,决定下一步调用哪个智能体,或者是否重复执行某个步骤。例如,如果CoderAgent生成的代码被CriticAgent评为“高风险”,则可能触发另一个SeniorCoderAgent进行重写。这需要在自定义Orchestrator的select_agent和should_continue方法中,加入对会话历史内容的分析逻辑。
并行执行:有些子任务可以并行。例如,在调研一个技术方案时,可以同时启动ArchitectAgent和ResearcherAgent(负责搜索最新资料)。Shogun的基础Session是顺序的,但你可以构建一个ParallelOrchestrator,利用asyncio或线程池并发调用多个智能体,然后聚合结果。
引入“规划”智能体:对于开放式的复杂任务,让一个专用的PlannerAgent(通常使用能力最强的 LLM,如 GPT-4)来首先分解任务,生成一个结构化的计划(例如 JSON 格式的任务列表),然后Orchestrator再根据这个计划来驱动其他智能体执行。这比在Orchestrator中硬编码流程要灵活和强大得多。
4.3 可观测性与调试
多智能体系统的“黑盒”特性比单智能体更甚。强大的可观测性工具是必不可少的。
结构化日志:不要仅仅打印文本。Session的每一次状态变更(智能体调用开始/结束、消息传递)都应该以结构化的格式(如 JSON)记录到日志系统或数据库中。记录的信息应包括:时间戳、会话ID、当前步骤、输入消息摘要、输出消息摘要、所用模型、Token 消耗、耗时、错误信息等。
可视化工具:考虑开发一个简单的 Web 界面,以时间线或流程图的形式实时展示会话过程,点击每个节点可以查看详细的输入输出。这对于向非技术人员演示和团队协作调试非常有帮助。
成本与性能监控:记录每个 LLM 调用的 Token 使用量和耗时。这有助于你发现性能瓶颈(哪个智能体最慢/最贵)并优化提示词或流程。
4.4 与其他工具的集成
Shogun管理的是智能体间的协作逻辑,但智能体本身需要“工具”来增强能力。
工具调用集成:让智能体能够调用外部工具,如计算器、搜索引擎、数据库、API 等。这通常通过让智能体输出一个结构化的“工具调用请求”(如符合 OpenAI Function Calling 或 ReAct 格式),然后在Session或一个专门的ToolExecutor中解析并执行该请求,将结果以Message的形式返回给智能体。Shogun的Message.metadata字段非常适合用来传递这类结构化数据。
向量数据库与长期记忆:如果智能体需要参考本次会话之外的知识(如公司文档、历史对话),就需要集成向量数据库(如 Chroma, Weaviate)。可以设计一个RetrievalAgent,其act方法就是根据查询从向量库中检索相关片段,并将结果作为上下文注入到后续流程中。
5. 常见问题与实战排坑指南
在实际使用multi-agent-shogun或自建类似系统时,我踩过不少坑。这里总结一份速查表,希望能帮你绕开这些弯路。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 智能体输出无关内容或拒绝执行 | 提示词不清晰,角色定义模糊,或上下文混乱。 | 1.检查角色描述:确保Agent的role描述具体、包含动词和领域关键词。2.精炼提示词:在 act方法的提示词中,用明确的指令格式(如“请按以下三点回答:1... 2... 3...”)。3.净化上下文:检查传递给智能体的 Message历史是否包含了无关或冲突的信息。实现上下文过滤或摘要。 |
| 会话陷入循环或无法终止 | 编排器逻辑有缺陷,或智能体输出触发了重复的路由决策。 | 1.设置最大轮次:在Session.run或Orchestrator.should_continue中加入轮次限制(如 max_turns=10)。2.检查终止条件:让 Orchestrator分析最新消息的内容,判断是否包含“最终答案”、“无法处理”等信号,或任务是否已达成。3.日志调试:详细记录每一轮的选择和原因,分析循环模式。 |
| 系统响应速度极慢 | 顺序执行导致,或某个智能体(如调用慢速API/复杂模型)成为瓶颈。 | 1.分析耗时:为每个智能体的act方法添加计时,找出瓶颈。2.考虑并行化:对于无依赖的子任务,使用并行编排器。 3.模型降级:对不那么关键的任务,使用更快、更便宜的模型(如 gpt-4o-mini替代gpt-4)。4.实现异步调用:使用 asyncio重构智能体调用,但要注意LLM客户端库是否支持异步。 |
| 最终输出质量不稳定 | 智能体间传递的信息有损耗,或评审/修正环节缺失。 | 1.强化上下文传递:确保关键信息(如架构决策、约束条件)在消息metadata中结构化传递,而不只是放在content文本里。2.引入评审与修正循环:在流程末尾固定加入一个 ReviewerAgent,如果评审不通过,则将批评意见和原始任务重新注入流程开头或中间环节进行迭代。3.多数表决或融合:对于关键问题,让多个同类型智能体独立回答,然后通过一个 VoterAgent或SummarizerAgent来整合最佳答案。 |
| Token 消耗爆炸,成本过高 | 会话历史过长,每次调用都携带全部历史。 | 1.实现上下文窗口管理:这是必须的。开发一个ContextCompressor模块,其核心算法可以是:a.摘要:用LLM对旧历史进行总结。 b.关键信息提取:只保留与当前任务强相关的历史消息(如工具调用结果、特定角色的决策)。 c.滑动窗口:只保留最近 N 条消息。 2.优化提示词:移除提示词中不必要的叙述性文字。 |
| 智能体调用外部工具失败 | 工具调用结果格式错误,或工具执行异常未处理。 | 1.结构化工具调用:强制要求智能体以指定JSON格式请求工具。在调用工具前,验证格式。 2.工具执行沙盒化:对于执行代码、访问网络等高风险工具,必须在安全的沙盒环境中运行,并有超时和资源限制。 3.完善的错误反馈:工具执行失败时,将清晰的错误信息返回给智能体,让它有机会重试或调整请求。 |
一个关键的避坑技巧:从简单开始,逐步增加复杂性。不要一开始就设计一个包含10个智能体、复杂条件分支的系统。先用两个智能体(比如一个“执行者”,一个“检查者”)跑通最基本的“提问-回答-检查”流程。确保这个简单流程的通信、日志、错误处理都是稳固的。然后再逐步加入第三个智能体(如“优化者”),并尝试简单的分支逻辑。这种迭代方式能帮你快速定位问题所在,避免在复杂系统中迷失方向。
multi-agent-shogun项目提供了一个优雅而克制的抽象,让你能专注于多智能体协作逻辑本身,而不是从零开始搭建通信框架。它的轻量级特性既是优点也是限制。对于研究、原型开发和中等复杂度的生产应用,它是一个绝佳的起点。当你的需求超出其范围时,你对其核心组件(Agent, Message, Session)的理解,也能轻松地帮助你迁移到更重量级的框架(如 AutoGen, LangGraph)或基于其理念构建自定义的解决方案。多智能体的世界充满挑战,但也蕴含着解决复杂问题的巨大潜力,从这个“将军”框架开始你的探索,是一个明智而高效的选择。
