用 Chainlit 构建多智能体聊天应用:multi_agent_orchestrator 搭配 Bedrock 与 Ollama 的实战指南
【免费下载链接】agent-squadFlexible and powerful framework for managing multiple AI agents and handling complex conversations项目地址: https://gitcode.com/GitHub_Trending/mu/agent-squad
本指南基于开源仓库agent-squad中的examples/chat-chainlit-app示例,完整讲解如何用 Chainlit(说明:此处仅指 Python 聊天 UI 框架 Chainlit,非仓库内部模块)与multi_agent_orchestrator编排框架搭建一个支持智能路由、流式输出与多轮对话的多智能体聊天应用。读完本文,你将掌握从环境准备、依赖安装、应用启动到核心代码拆解的全流程,并能把一个同时跑在 Amazon Bedrock(云上 Agent)与 Ollama(本地模型)之上的三 Agent 应用完整跑通。
该示例同时展示了本仓库 Python 版编排框架(位于 python/src/multi_agent_orchestrator)的典型用法:用 Bedrock 上的 Claude 模型做意图分类,将用户问题路由到"技术 Agent""旅行 Agent""健康 Agent"之一,其中健康 Agent 由本地 Ollama 的 Llama 3.1 模型驱动,形成"云 + 本地"混合的多 Agent 体系。
示例应用概览:三个 Agent 与一个分类器
在动手前,先看清这个示例要解决什么问题。应用启动后,用户输入任意一句话,系统会先经过一个分类器判断意图,再把请求交给最合适的 Agent 处理:
| 组件 | 名称 | 底层模型 | 运行位置 |
|---|---|---|---|
| 分类器 | BedrockClassifier | anthropic.claude-3-haiku-20240307-v1:0 | Amazon Bedrock |
| Agent | Tech Agent(技术) | anthropic.claude-3-sonnet-20240229-v1:0 | Amazon Bedrock |
| Agent | Travel Agent(旅行) | anthropic.claude-3-sonnet-20240229-v1:0 | Amazon Bedrock |
| Agent | Health Agent(健康) | llama3.1:latest | 本地 Ollama |
三个 Agent 的职责描述定义在 examples/chat-chainlit-app/agents.py 中,分类器正是依赖这些描述来做意图匹配(详见后文"分类器的工作原理"一节)。而示例给出的四类测试问题,正好覆盖了三种 Agent 及"多轮追问"场景:
- 问 Seattle 值得去的地方 → 应路由到Travel Agent(Bedrock);
- 问 Seattle 有哪些科技公司 → 应路由到Tech Agent(Bedrock);
- 问 Seattle 什么花粉导致过敏 → 应路由到Health Agent(本地 Ollama);
- 针对旅行 Agent 的上一轮回答继续追问 → 应保持在同一 Agent 继续多轮对话。
环境准备(Prerequisites)
按照 examples/chat-chainlit-app/README.md 的要求,运行该应用需要满足以下前置条件:
- Python 3.7 及以上版本,且
pip(Python 包安装器)可用; - 本地已安装并运行 Ollama,且已拉取 ollamaAgent.py 中指定的模型
llama3.1:latest,可通过ollama pull llama3.1:latest提前下载; - AWS 凭证与 Bedrock 模型访问权限:由于分类器与两个 Bedrock Agent 都调用
bedrock-runtime,需要在本机配置好 AWS 凭证(如环境变量AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION,或使用~/.aws/credentials)。从源码看,bedrock_classifier.py 在未显式传入region时会读取AWS_REGION环境变量,bedrock_llm_agent.py 同样如此;同时请确保你的账号已开通上述 Claude 3 Haiku / Sonnet 模型的调用权限。
安装依赖与启动应用
README 给出的安装与运行步骤如下,本文在保留原步骤的基础上补充了关键说明。
1. 克隆仓库(如尚未完成)
git clone <repository-url> cd <repository-directory>2. 创建并激活虚拟环境(推荐)
python -m venv venv激活虚拟环境:
Windows:
venv\Scripts\activatemacOS / Linux:
source venv/bin/activate
3. 安装依赖
进入示例目录并安装依赖:
pip install -r requirements.txtexamples/chat-chainlit-app/requirements.txt 中锁定的依赖如下:
| 依赖包 | 版本 | 作用 |
|---|---|---|
chainlit | 1.3.2 | 聊天应用前端 UI 与消息流框架 |
multi_agent_orchestrator | 0.1.2 | 本仓库 Python 版多 Agent 编排框架(PyPI 包) |
ollama | 0.3.3 | 调用本地 Ollama 服务的 Python 客户端 |
pydantic | 2.10.1 | 数据校验,Chainlit 运行时依赖 |
注意:
multi_agent_orchestrator==0.1.2是从 PyPI 安装的发布包,与仓库内 python 目录下的源码对应。如果你希望直接使用仓库源码,也可以将python/目录加入PYTHONPATH或按 python/README.md 的方式安装。
4. 运行应用
chainlit run app.py -w其中-w(watch)参数表示启用热重载,修改代码后应用自动重启,非常适合开发调试。应用启动后,Chainlit 会在本地开启一个 Web 聊天界面(默认端口8000),直接在浏览器中打开即可对话。
5. 其他注意事项
- README 提醒:确保
multi_agent_orchestrator或其他组件所需的环境变量、配置文件已正确设置(主要指 AWS 凭证,见上文); - 如果安装包时遇到问题,请确认 Python 与 pip 版本为最新,并优先在干净的虚拟环境中安装。
核心代码拆解:app.py 中的编排器初始化
应用入口 examples/chat-chainlit-app/app.py 是理解整个编排流程的关键。它依次完成了四件事:初始化分类器、初始化编排器、注册 Agent、绑定 Chainlit 事件。
分类器初始化
custom_bedrock_classifier = BedrockClassifier(BedrockClassifierOptions( model_id='anthropic.claude-3-haiku-20240307-v1:0', inference_config={ 'maxTokens': 500, 'temperature': 0.7, 'topP': 0.9 } ))这里用 Haiku 模型作为分类器(比 Sonnet 更便宜、更快),并显式配置了采样参数。对照 bedrock_classifier.py 的源码,inference_config各字段的含义与默认值为:
| 参数 | 示例中的值 | 源码默认值 | 说明 |
|---|---|---|---|
maxTokens | 500 | 1000 | 生成的最大 token 数 |
temperature | 0.7 | 0.0 | 采样温度,越高越随机;分类任务通常建议低值以保持确定性 |
topP | 0.9 | 0.9 | 核采样概率阈值 |
stopSequences | 未设置 | [] | 停止序列列表 |
model_id未指定时默认使用anthropic.claude-3-5-sonnet-20240620-v1:0(该常量定义在 types/types.py)。
编排器初始化与配置项说明
orchestrator = MultiAgentOrchestrator(options=OrchestratorConfig( LOG_AGENT_CHAT=True, LOG_CLASSIFIER_CHAT=True, LOG_CLASSIFIER_RAW_OUTPUT=True, LOG_CLASSIFIER_OUTPUT=True, LOG_EXECUTION_TIMES=True, MAX_RETRIES=3, USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED=False, MAX_MESSAGE_PAIRS_PER_AGENT=10 ), classifier=custom_bedrock_classifier )OrchestratorConfig的全部字段及源码默认值定义在 types/types.py 中,逐一说明如下:
| 配置项 | 示例值 | 源码默认值 | 作用 |
|---|---|---|---|
LOG_AGENT_CHAT | True | False | 是否打印每个 Agent 的对话历史 |
LOG_CLASSIFIER_CHAT | True | False | 是否打印分类器看到的对话历史 |
LOG_CLASSIFIER_RAW_OUTPUT | True | False | 是否打印分类器的原始输出 |
LOG_CLASSIFIER_OUTPUT | True | False | 是否打印"分类意图"结果(命中 Agent、置信度) |
LOG_EXECUTION_TIMES | True | False | 是否统计并打印各阶段执行耗时 |
MAX_RETRIES | 3 | 3 | 分类/调用的最大重试次数 |
USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED | False | True | 分类器未选中任何 Agent 时,是否回退到默认 Agent。示例置为False,即未命中时直接返回"无法处理"提示 |
CLASSIFICATION_ERROR_MESSAGE | 未设置 | None | 分类出错时的提示文本 |
NO_SELECTED_AGENT_MESSAGE | 未设置 | 内置兜底文案 | 未选中 Agent 时返回给用户的提示 |
GENERAL_ROUTING_ERROR_MSG_MESSAGE | 未设置 | None | 路由过程异常时的提示文本 |
MAX_MESSAGE_PAIRS_PER_AGENT | 10 | 100 | 每个 Agent 保留的最大消息对数,超出后裁剪历史 |
从 orchestrator.py 的源码可以看出,options也可以直接传字典,框架会按OrchestratorConfig的字段自动过滤无效键。若不传classifier,框架在检测到bedrock相关依赖可用时会默认创建BedrockClassifier,否则抛出异常要求显式提供分类器。
注册 Agent
orchestrator.add_agent(create_tech_agent()) orchestrator.add_agent(create_travel_agent()) orchestrator.add_agent(create_health_agent())add_agent会把 Agent 加入内部字典self.agents,并同步调用classifier.set_agents(self.agents)——这一步把每个 Agent 的id与description注入分类器的提示词模板,是"智能路由"的数据来源(对应 classifier.py 中的set_agents方法)。
Chainlit 事件绑定
@cl.on_chat_start async def start(): cl.user_session.set("user_id", str(uuid.uuid4())) cl.user_session.set("session_id", str(uuid.uuid4())) cl.user_session.set("chat_history", []) @cl.on_message async def main(message: cl.Message): ...on_chat_start:每次新建会话时生成唯一的user_id与session_id,它们会作为编排器路由与历史存储的维度(见后文);on_message:收到用户消息后先发送一个空消息占位(msg.send()),随后调用编排器核心方法:
response: AgentResponse = await orchestrator.route_request(message.content, user_id, session_id, {})route_request是编排框架的总入口。对照 orchestrator.py 的源码,其内部调用链为:
classify_request(...):拉取该会话的全部 Agent聊天历史,调用分类器判定意图;- 若无选中 Agent:按
USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED决定是否回退默认 Agent,否则返回内置的"无法处理"消息; agent_process_request(...):通过dispatch_to_agent把请求交给选中 Agent 处理,并先后把用户消息、Agent 回复写入会话存储;finally中按LOG_EXECUTION_TIMES打印各阶段耗时。
流式响应输出
if isinstance(response, AgentResponse) and response.streaming is False: if isinstance(response.output, str): await msg.stream_token(response.output) elif isinstance(response.output, ConversationMessage): await msg.stream_token(response.output.content[0].get('text')) await msg.update()AgentResponse的streaming字段来自selected_agent.is_streaming_enabled()(对应 agent.py 中AgentResponse数据类与基类的is_streaming_enabled)。由于示例中三个 Agent 均开启了流式,真正的 token 级流式输出由各 Agent 的回调完成,这里只需在结束时update()刷新消息。
三 Agent 定义与流式回调:agents.py
examples/chat-chainlit-app/agents.py 定义了三个工厂函数,并实现了一个把 token 实时转发到 Chainlit 界面的回调类:
class ChainlitAgentCallbacks(AgentCallbacks): def on_llm_new_token(self, token: str) -> None: asyncio.run(cl.user_session.get("current_msg").stream_token(token))这是示例中最值得借鉴的集成技巧:AgentCallbacks是框架定义的钩子接口(见 agent.py),LLM 每次吐出新 token 时都会被调用。回调里从 Chainlit 的user_session取出当前消息对象,用asyncio.run把 token 流式推送到前端——因为回调是同步的,而stream_token是异步方法,所以需要asyncio.run桥接。注意app.py在on_message中提前执行了cl.user_session.set("current_msg", msg),回调才能取到它。
三个 Agent 的定义要点:
def create_tech_agent(): return BedrockLLMAgent(BedrockLLMAgentOptions( name="Tech Agent", streaming=True, description="Specializes in technology areas including software development, hardware, AI, ...", model_id="anthropic.claude-3-sonnet-20240229-v1:0", callbacks=ChainlitAgentCallbacks() ))- Tech Agent / Travel Agent:均使用
BedrockLLMAgent,模型为 Claude 3 Sonnet,streaming=True开启流式; - Health Agent:使用自定义的
OllamaAgent,模型为llama3.1:latest,同样开启流式; description是关键:这段文本既是 Agent 的系统提示词素材,也是分类器选择 Agent 的依据。从 bedrock_llm_agent.py 源码可以看到,BedrockLLMAgent会把name与description拼进默认提示词模板("You are a {name}. {description}...")作为系统提示词;同时分类器模板中的AGENT_DESCRIPTIONS也由各 Agent 的id: description拼接而成。描述写得越精准、边界越清晰,路由准确率越高。
本地 Agent 实现:ollamaAgent.py
健康 Agent 使用的是示例自定义的 examples/chat-chainlit-app/ollamaAgent.py,它继承框架基类Agent并包装 Ollama Python 客户端:
@dataclass class OllamaAgentOptions(AgentOptions): streaming: bool = True model_id: str = "llama3.1:latest" class OllamaAgent(Agent): async def process_request(self, input_text, user_id, session_id, chat_history, additional_params=None): messages = [ {"role": msg.role, "content": msg.content[0]['text']} for msg in chat_history ] messages.append({"role": ParticipantRole.USER.value, "content": input_text}) if self.streaming: return await self.handle_streaming_response(messages) else: response = ollama.chat(model=self.model_id, messages=messages) return ConversationMessage(role=ParticipantRole.ASSISTANT.value, content=[{"text": response['message']['content']}])要点解读:
- 历史消息重组:框架传入的
chat_history是ConversationMessage列表,需要先转成 Ollama 所需的[{"role": ..., "content": ...}]字典列表,再追加当前用户输入; - 流式分支:
handle_streaming_response中以stream=True调用ollama.chat,逐段累加内容并同步触发self.callbacks.on_llm_new_token(...)——这正是 Chainlit 界面逐字输出的来源; - 非流式分支:直接取
response['message']['content']包装成ConversationMessage。
这个文件的价值在于演示了如何为框架扩展一个全新的本地模型 Agent:只要继承Agent、实现process_request,并在流式路径中正确触发回调,即可无缝接入编排器,与 Bedrock Agent 平级共存。
分类器的工作原理:从 AgentMatcher 到工具调用
为什么同一个route_request能自动把"花粉过敏"分给健康 Agent、把"科技公司"分给技术 Agent?答案藏在分类器的提示词与工具调用机制中。
AgentMatcher 系统提示词
classifier.py 内置了一套名为AgentMatcher的系统提示词模板,核心逻辑包括:
- 根据用户输入从
<agents>列表中挑选最合适的 Agent 类型; - 特殊处理追问:模板明确要求"如果用户输入是上一轮对话的延续(如 yes、ok、I want to know more、数字答案等),则沿用上一轮选中的 Agent"——这正是 README 中第 4 个测试问题(追问旅行 Agent)能保持在同一 Agent 的原因;
- 对每个候选输出
userinput、selected_agent、confidence字段; - 模板中
{{AGENT_DESCRIPTIONS}}与{{HISTORY}}两个占位符,分别由set_agents注入的 Agent 描述列表和set_history格式化的历史消息填充(对应update_system_prompt方法)。
结构化的工具调用
bedrock_classifier.py 在调用模型时定义了一个名为analyzePrompt的 Bedrock tool,输入 Schema 包含userinput、selected_agent、confidence三个字段。对 Anthropic 系模型还会设置toolChoice强制模型调用该工具;拿到toolUse后,通过get_agent_by_id(tool_use['input']['selected_agent'])映射回具体 Agent,并携带置信度构造ClassifierResult。也就是说,分类结果不是自由文本,而是经过工具调用产出的结构化数据,这让路由结果稳定、可解析。
路由兜底
在 orchestrator.py 的classify_request中,若selected_agent为空且配置了USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED=True,会通过get_fallback_result()回退到default_agent。示例中该选项为False,因此无法识别的问题会得到"无法确定如何处理"的礼貌回复。
会话历史与多轮追问:存储层如何工作
示例没有显式传入storage,因此框架默认使用InMemoryChatStorage(内存存储,见 in_memory_chat_storage.py)。它按user_id#session_id#agent_id作为 key 组织对话,特点包括:
- 按 Agent 隔离历史:每个 Agent 只看到自己参与过的对话(
fetch_chat按 Agent 维度取历史),而分类器通过fetch_all_chats看到该会话下所有 Agent 的完整历史,并给助手消息加上[agent_id]前缀,以便判断当前对话上下文; - 去重与裁剪:连续相同角色(如两条相邻 user 消息)不重复保存;保存时按
MAX_MESSAGE_PAIRS_PER_AGENT裁剪历史(示例配置为 10 对); - 时间戳排序:消息带毫秒时间戳,
fetch_all_chats会按时间排序,保证多 Agent 交错对话的顺序正确。
这也解释了 README 中"追问旅行 Agent"测试问题的实现基础:你的追问会与之前旅行 Agent 的上下文一起进入分类器,分类器据历史沿用原 Agent,再把完整历史交给该 Agent 生成连贯的续答。如果你需要持久化(如跨会话保留历史),框架还提供了 DynamoDB 存储 与 SQL 存储 等实现,可参考 storage 概览文档。
完整运行验证与预期效果
按前面步骤启动后,可用 README 给出的四类问题依次验证:
- 输入"What are some best places to visit in Seattle?",观察终端日志中
Classified Intent显示命中travel-agent,浏览器收到 Claude 3 Sonnet 生成的旅行建议(流式输出); - 输入"What are some cool tech companies in Seattle",应命中
tech-agent; - 输入"What kind of pollen is causing allergies in Seattle?",应命中
health-agent,由本地 Ollama 的 Llama 3.1 流式回答——这也是验证 Ollama 服务与模型是否就绪的最快方式; - 针对第 1 问继续追问(如"Tell me more about the food there"),应保持路由到
travel-agent并参考上一轮上下文作答。
调试时善用app.py中开启的日志开关:LOG_CLASSIFIER_OUTPUT=True会打印命中的 Agent 与置信度,LOG_EXECUTION_TIMES=True会打印"意图分类"与"Agent 处理"两个阶段的耗时,方便快速定位路由不准或响应缓慢的问题。
常见问题与排查建议
- 找不到
multi_agent_orchestrator模块:确认requirements.txt已安装,或检查 Python 解释器是否为当前虚拟环境; - 分类器报 AWS 相关错误:确认 AWS 凭证已配置且 Bedrock 模型可用,可先单独用
boto3调用一次converse验证权限; - 健康 Agent 无响应或报连接错误:确认 Ollama 服务已启动(
ollama serve)且已拉取llama3.1:latest; - 路由结果不稳定:尝试调低分类器的
temperature(分类任务推荐接近 0),或细化各 Agent 的description边界; - 追问没有保持同一 Agent:检查
MAX_MESSAGE_PAIRS_PER_AGENT是否过小导致历史被裁剪,历史不足时分类器难以判断上下文。
至此,一个"Bedrock 云上 Agent + Ollama 本地 Agent + Claude 智能路由 + Chainlit 流式界面"的完整多智能体应用就搭建完成了。你可以在此基础上继续探索本仓库的其他 Agent 类型(如 Amazon Bedrock Agent、Bedrock Flows)以及 编排器更多用法,把它扩展成适合自己业务场景的多 Agent 助手。
【免费下载链接】agent-squadFlexible and powerful framework for managing multiple AI agents and handling complex conversations项目地址: https://gitcode.com/GitHub_Trending/mu/agent-squad
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考