Mistral-入门指南-二-

Mistral 入门指南(二)

原文:Learn Mistral

译者:飞龙

协议:CC BY-NC-SA 4.0

此代码首先列出你Pinecone项目中的所有现有索引。如果找到'dish-embeddings',则将其删除以确保我们不会积累过时或重复的数据。重新创建索引比更新现有记录更快,而且对于相同工作流的重复运行非常理想。

一旦索引创建并配置完成,我们就可以向其填充数据了。下一步是插入我们之前为每个菜肴生成的嵌入。这将允许Pinecone在过程的后期存储并组织它们以进行快速相似性搜索。

将嵌入写入 Pinecone

index = pcone.Index('dish-embeddings')
for i, embed in enumerate(dish_embeds_arr):
    index.upsert([(f'dish_{i}', embed)])

此代码获取我们之前创建的名为dish-embeddings的索引。通过名称引用索引,我们可以执行各种操作,如插入、查询和删除记录。

循环将每个嵌入插入索引。以下行遍历dish_embeds_arr列表,其中i是嵌入的索引,embed是嵌入向量本身。

for i, embed in enumerate(dish_embeds_arr):

index.upsert([(f'dish_{i}', embed)])方法用于在索引中插入或更新记录。该方法接受一个元组列表,每个元组包含一个 ID 和对应的嵌入向量。在这种情况下,我们使用f'dish_{i}'为每个嵌入向量创建一个唯一 ID(例如'dish_0'、'dish_1'等),而embed是实际的嵌入向量。

upsert方法用于插入或更新记录。如果已经存在相同 ID 的记录,它将用新的嵌入向量对其进行更新。如果不存在,将创建一个新记录。为了提高效率,特别是在处理大量嵌入向量时,请考虑对upsert操作进行批处理。这涉及到将多个嵌入向量分组到单个upsert调用中,以减少对服务器的请求次数。

通过查找索引并使用upsert方法,我们可以将菜肴嵌入向量插入 Pinecone 索引中。这种设置允许我们高效地管理和查询高维向量数据。

当所有菜肴嵌入向量都成功插入索引后,我们现在准备好执行该系统的构建初衷——语义搜索。在下一步,我们将获取用户查询,将其转换为嵌入向量,并搜索 Pinecone 索引以找到语义上最相似的菜肴。这就是向量搜索展现其真实威力的地方。

寻找最匹配项

要找到与用户偏好最匹配的菜肴,我们使用以下代码在 Pinecone 索引中搜索最近的向量:

result = index.query(vector=[user_embed], top_k=1)
closest_dish_id = result['matches'][0]['id']
closest_dish_index = int(closest_dish_id.split('_')[1])
print(closest_dish_index)
print(dish_descriptions[closest_dish_index])

这段代码执行了以下操作:

  • 首先,我们查询 Pinecone 索引以找到与用户嵌入向量最近的向量。result = index.query(vector=[user_embed], top_k=1)这一行使用用户的嵌入(user_embed)向索引发送查询。vector=[user_embed]参数指定了我们用于查询的嵌入,而top_k=1表示我们想要单个最接近的匹配项(即最近邻居)。

  • 接下来,我们从查询结果中提取最匹配项的 ID 并将其转换为索引。该 ID 是像'dish_8'这样的字符串;我们对其进行拆分以数字 8,然后使用该数字从列表中检索匹配的菜肴。

  • 最后,我们打印最匹配的索引,然后使用该索引从菜肴列表中检索并显示其描述。

代码输出了以下内容:

8
Cheeseburger with fries

输出显示项目 8 “Cheeseburger with fries”(薯条芝士堡)是与用户输入“hot dog”(热狗)最接近的语义匹配项。这一结果反映了嵌入向量是如何捕捉上下文相似性的——两种菜肴都是经典的快餐,具有相似的配料和场景,使得它们在向量空间中密切相关。

随着我们继续加深对嵌入的理解,我们将转向这些高维表示的可视化。接下来,你将探索热力图和 t-SNE 图等实用技术,以便以更直观、人类可读的形式更好地解释和比较向量之间的关系。

额外练习:带热力图可视化的语义相似性

为了可视化两个句子中单词之间的语义相似性,我们将为每个单词生成嵌入,然后计算这些嵌入之间的余弦相似度。我们将使用这些相似性评分创建一个热力图,帮助我们理解单词之间的关系。

首先,我们定义句子以及其中的单词。然后,我们使用 Mistral 模型为每个单词生成嵌入。获取嵌入后,我们对其进行清理并准备用于进一步处理的数组。

我们计算两个句子中每对单词之间的余弦相似度。最后,我们使用相似性评分创建并显示热力图,提供单词之间语义相似性的视觉化表示。

本节省略了逐行描述,因为假设你到这里已经熟悉了大部分代码。

import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics.pairwise import cosine_similarity

定义句子和单词

sentence1 = ["I", "enjoy", "spicy", "food"]
sentence2 = ["She", "likes", "hot", "meals"]

为每个单词生成嵌入

embeddings1 = client.embeddings.create(
    model='mistral-embed', inputs=sentence1)
embeddings2 = client.embeddings.create(
    model='mistral-embed', inputs=sentence2)

清理嵌入数组

embeddings1_arr = []
embeddings2_arr = []

for embed in embeddings1.data:
    embeddings1_arr.append(embed.embedding)
for embed in embeddings2.data:
    embeddings2_arr.append(embed.embedding)

计算每对单词之间的余弦相似度

similarity_scores = cosine_similarity(embeddings2_arr, embeddings1_arr)

创建热力图

plt.figure(figsize=(8, 6))
sns.heatmap(
    similarity_scores, annot=True, cmap='Blues', 
    xticklabels=sentence1, yticklabels=sentence2
)

设置标题和标签

plt.title('Semantic Similarity Heatmap')
plt.xlabel('Words in Sentence 1')
plt.ylabel('Words in Sentence 2')

显示热力图

plt.show()

tsne_results = tsne.fit_transform(dish_embeds_np_arr)

  • perplexity=5 控制 t-SNE 如何平衡局部结构与全局结构

  • random_state=42 确保结果的可重复性

  • fit_transform() 执行实际的降维,为每个嵌入(embedding)生成一个 2D 坐标

最后,我们绘制 2D 投影图:

plt.figure(figsize=(10, 7))

for i, label in enumerate(dish_descriptions):
    x, y = tsne_results[i, :]
    plt.scatter(x, y)
    plt.text(x + 0.1, y + 0.1, label, fontsize=9)
plt.title('t-SNE Visualization of Dish Embeddings')
plt.xlabel('t-SNE Dimension 1')
plt.ylabel('t-SNE Dimension 2')
plt.show()

代码段遍历每个菜肴描述及其对应的 2D 坐标,然后为每个嵌入(embedding)绘制一个点(散点图),并在点略微偏移处叠加文本标签。最后,它标记了轴线并显示带有描述性标题的图表。图表已准备就绪!

可视化代码的输出显示在以下生成的图中:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_04_12.png

图 4.12: t-SNE 可视化代码输出

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/1.png快速提示:需要查看此图像的高分辨率版本吗?请在下一代 Packt Reader 中打开此书,或在 PDF/ePub 版本中查看。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/2.png购买此书时将免费赠送下一代 Packt Reader。扫描 QR 码或访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。双检显示的版本以确保您获得正确的版本。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/Unlock2.png

正如我们所看到的,Cheeseburger(芝士汉堡)离 hot dog(热狗)最近。我们带着良好的视觉结果和丰富的知识达到了工作坊的结尾。

工作坊完成!

你现在已经完成了工作坊的所有核心步骤——做得好。从初始化 Mistral 和 Pinecone 客户端到生成、存储和查询嵌入,你已经使用现代向量的 AI 工具构建了一个功能完备的语义搜索系统。在此过程中,你处理了用户输入处理、向量相似性匹配,甚至探索了如何使用向量数据库扩展系统。这段实践性旅程让你既掌握了技术工作流,又理解了嵌入在实际应用中背后的概念。

但这仅仅是一个开始。接下来,我们将探索这些概念如何应用于真实世界的场景中,从推荐系统到文档搜索,并引导你完成一些实性练习,这些练习将挑战你把所学知识从受控示例扩展到具有实际意义的应用中。

嵌入的应用与实战练习

现在你已经掌握了生成和查询嵌入的机制,是时候将这些技能应用于真实世界的问题了。在本节中,我们将探索实际的用例,例如语义文档搜索、个性化推荐和 FAQ 匹配,在这些领域中嵌入增加了切实的价值。每个示例都配有一个练习挑战,旨在帮助你练习实现解决方案。

在深入开始练习之前,请以系统设计者的身份对待每个问题。请记住这些步骤:

  • 定义问题:清晰地概述你想要解决的问题,无论是将职位描述与候选人匹配、对文档分类,还是开发推荐系统。

  • 收集数据:为你的应用程序收集必要的数据。例如,收集职位描述和候选人简历,或者收集文档或项目的数据集。

  • 生成嵌入:使用预训练模型为你的数据生成嵌入。

  • 存储并查询嵌入:选择向量数据库如 Pinecone、Neo4j 或 FAISS 来存储并高效查询嵌入。

  • 实现解决方案:编写代码使用嵌入和所选数据库来解决问题。使用 cosine similarity(余弦相似度)或其他适当距离度量来比较嵌入。

  • 评估与迭代:测试你的解决方案,评估其性能并根据需要进行改进。

职位描述与候选人匹配服务

语义搜索可以通过理解底层的技能、经验和要求,将职位描述与候选人简介进行匹配。嵌入有助于识别那些技能和经验在语义上与职位要求的候选人,即使精确的关键词并不匹配。这将带来更高效、更准确的招聘过程,帮助雇主找到最佳候选人,帮助求职者找到适合其专长的职位。

文档分类服务

嵌入可以通过捕捉语义内容将文档归类为预定义的类别。这对于组织大型文档集(如法律摘要、学术论文和新闻文章)特别有用。可以使用网页爬虫库中的 LlamaIndex 和 Smart PDF Loader 等服务从网络或 PDF 文件收集并预处理文档,提取相关的文本和元数据。

嵌入确保每个文档根据其内容被准确分类,实现高效的信息检索和管理。对于长文档,通常需要将其拆分为具有重叠的较部分以保留上下文。Python 库如 chunkipy 对此任务很有帮助。

推荐系统

嵌入被用于增强产品、电影、音乐等的推荐系统。通过在更深语义层面上理解用户偏好和物品特征,嵌入可以根据过去的行为和相似的用户画像推荐用户可能喜欢的物品。这种个性化方法提升了用户的满意度和参与度。

新闻分析与与股价的相关性

语义搜索可用于分析新闻文章并并将其与股价波动相关联。通过对新闻内容和财务数据进行嵌入,算法可以识别模式并预测某些新闻事件可能如何影响股价。LlamaIndex 等工具可以从各种来源收集实时新闻数据,嵌入可以处理并分析这些数据以提供对市场趋势的洞察。该应用对于寻求根据最新新闻做出明智决策的投资者和金融分析师来说非常有价值。

这些实践练习将帮助你将嵌入的理论概念应用于现实问题,增强你的理解和技能。无论你选择预定义的应用还是想出自己的,这种经历对于掌握在各个领域使用嵌入都是无价的。投入其中,进行实验,发现嵌入的变革力量!

总结

本章带你从嵌入的基础走向它们的实际应用。你学习了嵌入如何捕捉语义含义,如何使用它们进行相似性比较,以及 Pinecone 等工具如何支持解决方案。通过实践练习,你获得了生成、存储和查询嵌入的经验——这些技能可以驱动个性化的系统。嵌入不仅仅是向量——它们解锁了 AI 驱动的应用。无论你是在增强搜索、构建助手还是分析内容,这些知识都推动了创新。下一章关注智能体(agents)——通过使用工具、做出决策和推理完成复杂任务。这是构建动态工作流的重要一步。

课外拓展

在建立了处理嵌入的坚实基础后,你现在准备好探索更高级、创意的应用了。让这些练习为你的项目带来灵感:

  1. 尝试不同的数据库:虽然 Pinecone 是强大的向量数据库,但还有其他可以探索,如 FAISS、Annoy、Milvus。每个数据库都有自己的优势和用例。

  2. 探索不同的距离度量:cosine similarity 常用于比较嵌入,但其他距离指标如 Euclidean distance 和 dot-product similarity 同样有用。尝试不同的指标看看它们如何影响结果并理解哪种最适合你的数据。

  3. 理解嵌入生成的机制:深入研究嵌入背后的算法和神经网络架构。探索 BERT、GPT 和 Word2Vec 等模型如何创建嵌入,以及支撑这些过程的数学原理。理解底层发生了什么有助于在各种上下文中优化嵌入。

  4. 实现自定义嵌入:通过在专用数据集上训练模型,为特定用例创建自己的嵌入。在预训练模型可能捕捉微妙之处的情况下尝试不同的训练技术并评估性能。

  5. 高级可视化技术:除了 t-SNE,探索其他降维和可视化技术,如 Uniform Manifold Approximation (UMAP) 或 Principal Component Analysis (PCA)。

  6. 与其他 AI 技术集成:将嵌入与 Natural Language Processing (NLP)、计算机视觉或强化学习结合以创建系统。例如,将嵌入与 NLP 集成可以增强机器人,而与计算机视觉结合可以改进搜索和分类。

  7. 现实应用开发:使用嵌入开发完整的端到端应用程序。这可以是从推荐系统到搜索引擎的个人爱好项目。记录过程、挑战和解决方案以创建一个全面的案例研究。

| 立即解锁此书的专属权益:扫描此 QR 码或访问 packtpub.com/unlock,然后通过名称搜索此书。 | https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/Unlock-01.pnghttps://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/Unlock2.png |

| 注意:在开始之前准备好你的购买发票。 |

5 智能体:从自动化到智能

产生想法是一回事,Gromit——让它们运行是另一回事!

——Wallace & Gromit (Nick Park, Aardman Animations)

让我们从一个火花开始:本章关于智能体(agents)。不是科幻中的机器人,而是能够思考、适应并完成工作的真实系统。如果传统的自动化感觉像直行,那么智能体就是在学习与复杂性共舞。

接下来的理论简洁集中,旨在提供足够的深度来理解为什么智能体很重要、它们如何工作以及它们与旧方法的区别。每个部分都面向行动,让你实验、构建和评估自己的智能体。到结束时,你将看到智能体不仅仅是噱,而是下一次大飞跃。在本章中,你将完成:

  • 定义智能体并理解工具、内存和推理循环等核心组件

  • 工作坊 1:多智能体回答服务

  • 工作坊 2:使用 n8n 的视觉 AI 智能体

技术要求

让我们通过准备好工作来为成功奠定基础。你需要准备一些基本的工具和账户,以便跟随本章中的示例和实验:

  • Mistral 账户:拥有 Mistral 账户对于访问高级语言模型以及训练和部署大规模语言模型所需的工具和资源至关重要。

  • Google Colab 或本地 Jupyter 服务器:Google Colab 提供了一个云平台,可以运行免费访问 GPU 和 TPU 的 Jupyter 笔记本,适用于执行密集计算。或者,本地 Jupyter 服务器允许在自己的硬件上运行这些笔记本,对计算环境拥有完全控制权。

  • GitHub 仓库:GitHub 仓库 中的 Chapter-5 文件夹(位于 [github.com/PacktPublishing/Learn-Mistral]( https://github.com/PacktPublishing/Learn-Mistral ))包含了第五章的所有代码和资源。该仓库是下载章节中讨论的代码段和示例的价值资源。

问题描述

传统的自动化已经业务向前走了很多,但其僵性已显露无遗。传统的自动化在压力下挣扎——当边缘情况出现时,工作流就会失败。在当今动态环境中,很少存在预测性;输入在变化,数据在移动,异常不断。大语言模型 (LLMs) 提供了智能,但缺乏持久性、内存结构。挑战是显而易见的:我们需要一种将推理、适应性整合工作流的方法。智能体解决了这一空白,将生成模型转变为以目标为导向的问题解决系统。

定义 AI 系统中的智能体

本节揭开了智能体系统的面纱,并奠定了基础:智能体是什么、它们如何运作以及它们与旧方法的区别。通过工具、内存和推理循环等核心组件,解锁更深理解。智能体不仅仅是回答,无论是增强搜索、构建助手还是分析内容,这些知识驱动了 AI 驱动的应用。你将学习智能体如何使用工具、做出决策并通过推理完成任务。这种持续循环将原本被动的 LLM 转化为适应性系统。

!pip install langchain langchain-mistralai requests beautifulsoup4 chromadb faiss-cpu sentence-transformers ipywidgets
print("✅ All packages installed successfully!")

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/3.png 快速提示:使用 AI Code Explainer 和 Quick Copy 功能增强您的编码体验。请在下一代 Packt Reader 中打开此书。点击 Copy 按钮(1)可以将代码快速复制到您的编码环境中,或点击 Explain 按钮(2)让 AI 助手为您解释一段代码。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/5.png

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/21.png 购买此书将免费赠送下一代 Packt Reader。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。双击检查显示的版本以确保获取的是正确的版本。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/Unlock2.png

基础工作完成后,我们现在可以通过安全加载 API 密钥、初始化聊天模型并测试响应,配置与 Mistral 的连接。

步骤 2:准备 Mistral API 密钥并测试连接

基础工作完成后,现在是时候将注意力转移到连接 Mistral API 了:

  1. 我们首先导入所需的模块和核心库。这些模块允许我们处理环境变量并与 Mistral 聊天客户端交互:
from google.colab import userdata
import os
from langchain_mistralai import ChatMistralAI
  1. 随后我们初始化 API 密钥以验证请求并开启对 Mistral 服务的安全访问。API 密钥从 Colab 用户数据中安全检索并放入环境变量,以便其他组件可以使用它:
# 安全地获取 Mistral API key
# 从 https://console.mistral.ai/ 获取您的免费 Mistral API 密钥
mistral_api_key = userdata.get('MISTRAL_API_KEY')
os.environ["MISTRAL_API_KEY"] = mistral_api_key
  1. 凭据就绪后,下一步是设置 LLM 客户端、配置模型参数并准备其有效处理处理查询。如下列表所示,客户端使用选定的模型、温度和最大 token 长度进行初始化:
# 初始化 Mistral LLM
llm = ChatMistralAI(
    model="mistral-small",
    mistral_api_key=mistral_api_key,
    temperature=0.7,
    max_tokens=1000
)
  • model="mistral-small" 选择了一个适用于快速原型构建的紧凑模型

  • mistral_api_key 将客户端绑定到您的账户

  • temperature=0.7 提供了平衡的创造力

  • max_tokens=1000 设置了响应的上限

  1. 一旦客户端准备就绪,我们就运行一个快速测试查询以确认连接正常且模型按预期做出响应。我们发送一个短提示来检查 LLM 是否响应正确,同时捕获可能出现的错误:

测试连接

try:
    response = llm.invoke("Hello! Please confirm you're working")
    print("✅ Connected to Mistral successfully!")
    print(f" Response: {response.content}")
except Exception as e:
    print(f"❌ Connection failed: {str(e)}")
    print("Please check your API key and try again.")

一旦客户端响应,连接即被完全确认。准备阶段完成。现在,可以开始深入添加智能体可以调用的外部工具(例如计算器或解析器),并在将它们组装成最小代理循环之前进行。

创建安全的计算器工具

在满足先决条件后,让我们开启智能体可以调用的第一个工具门。我们导入原始类,定义一个安全的算术助手,将其封装为 LangChain 工具,然后用一些直接表达式对其进行测试。

让我们开始吧:

  1. 我们首先引入用于工具、代理和提示词模板的 LangChain 类。这些导入提供了核心类——用于定义可重用函数的Tool,构建代理的create_react_agent和AgentExecutor,以及用于拉取标准提示词模板的hub:
from langchain.tools import Tool
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
  1. 接着,我们创建一个助手函数来验证表达式并确保基础数学运算的安全执行。该助手函数会去除空格、检查无效字符、拒绝长表达式,并在处理错误的同时评估基础数学。该设计优先考虑安全性和受控的评估,防止格式错误的表达式破坏工作流:
def safe_calculate(expression: str) -> str:
    """安全地评估数学表达式"""
    try:
        expression = expression.strip()
        allowed_chars = set('0123456789+-*/()%. ')
        if not all(c in allowed_chars for c in expression):
            return "Error: Invalid characters in expression. Only numbers and +, -, *, /, (, ), %, . are allowed."
        if len(expression) > 100:
            return "Error: Expression too long"
        result = eval(expression)
        return f"Result: {result}"
    except ZeroDivisionError:
        return "Error: Division by zero"
    except SyntaxError:
        return "Error: Invalid mathematical expression syntax"
    except Exception as e:
        return f"Error: {str(e)}"
  1. 现在,我们将安全的计算器暴露给 LangChain 作为一个智能体稍后调用的工具,如下列表所示:
calculator_tool = Tool(
    name="calculator",
    description="Safely evaluate math expressions like '2+2', '10*5', or '(15+25)/2'. Only basic operations are supported.",
    func=safe_calculate
)
  • name: 智能体调用工具时使用的唯一标识

  • description: 对 LLM 的引导,确保仅在支持数学表达式时调用该工具

  • func: 实现逻辑的 Python 函数——在这里是safe_calculate

  1. 工具定义好后,下一步是验证它是否能独立工作。此时,我们可以用该工具尝试几个示例表达式,以验证它运行正常并返回预期的输出:
test_expressions = ["2+2", "10*5", "15/3", "(10+5)*2"]
print("\n🧪 直接测试计算器工具:")
for expr in test_expressions:
    result = calculator_tool.func(expr)
    print(f"  {expr} = {result}")

此循环遍历表达式,调用工具并打印结果。这在将其嵌入代理之前,提供了一个对计算器行为是否符合预期的快速检查。

在落实基础之后,下一阶段是将此工具与推理循环相结合以扩展智能体的能力。

使用共享的 ReAct 提示词并精炼代理循环

基础工作就绪后,我们现在可以转向从 LangChain Hub 采用标准的 ReAct 提示词。这为智能体提供了一个用于工具使用和答案合成的一致推理模板:

  1. 我们首先获取一个编码了 ReAct 推理格式的精选提示。该模板为智能体提供了结构化的推理指令,用于逐步使用工具并生成答案:
react_prompt = hub.pull("hwchase17/react")
  1. 接下来,我们将计算器工具与提示词结合,创建一个新的代理和执行器来管理其行为,如下列表所示:
tools = [calculator_tool]
agent = create_react_agent(llm, tools, react_prompt)
calculator_agent = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=5,
    max_execution_time=30,
    handle_parsing_errors=True,
    early_stopping_method="generate"
)
  • agent: 由模型、工具和 ReAct 提示创建的核心推理循环

  • tools: 可用工具列表——在这里,只有计算器

  • verbose=True: 打印逐步推理的足迹

  • max_iterations=5: 将推理循环限制为五步

  • max_execution_time=30: 整体执行的安全限制(秒)

  • handle_parsing_errors=True: 如果大语言模型(LLM)生成了格式错误的输出,则允许恢复

  • early_stopping_method="generate": 决定在达到最大迭代次数时如何优雅地停止

  1. 设置好agent之后,是时候测试它在自然语言查询上的行为了。在这里,我们为智能体提供了日常语言提示词,并观察它如何使用calculator工具来生成答案:
test_queries = [
    "What is 25 multiplied by 47?",
    "Calculate 150 divided by 6",
    "What's 15 percent of 200?"
]

for query in test_queries:
    print(f"\n{'='*60}")
    print(f" Query: {query}")
    print('='*60)
    try:
        result = calculator_agent.invoke({"input": query})
        if 'output' in result:
            print(f"✅ Agent Response: {result['output']}")
        else:
            print(f"⚠️  Agent Response: {result}")
    except Exception as e:
        print(f"❌ Error: {str(e)}")
        if "25" in query and "47" in query:
            print(f"🔧 Fallback calculation: 25 * 47 = {25 * 47}")
        elif "150" in query and "6" in query:
            print(f"🔧 Fallback calculation: 150 / 6 = {150 / 6}")
        elif "15" in query and "200" in query:
            print(f"🔧 Fallback calculation: 15% of 200 = {200 * 0.15}")

在此阶段,循环在自然语言查询上进行了测试,将它们映射为工具调用。错误处理确保了如果智能体表现不当,会打印回退计算,展示了推理、工具使用和输出之间是如何连接的。

构建网页搜索工具

这是一个简单的工具概念,但智能体框架(agent frameworks)可以成长得更为强大。想象一下,工具不仅可以计算,还能打开你房间的灯、代表你交易股票,或设计完整的旅行行程。潜在列表几乎是无尽的。作为朝此方向的第一步,让我们通过将外部知识访问(从简单的网页搜索开始)纳入现有智能体框架来扩展工作流:

# 我们引入了用于 HTTP 用的 requests、用于结构化输入的 typing 和 pydantic,以及作为新工具基础的 LangChain BaseTool。此设置为定义输入模式、创建工具类和发送网络请求提供了构建块。
import requests
from langchain.tools import BaseTool
from typing import Optional, Type
from pydantic import BaseModel, Field
import json

我们接着定义了一个仅包含一个字段的模式——一个代表将用于搜索的查询字符串。该模式确保了当工具被调用时,它接收的是一个有效且描述清晰的查询字符串:

class WebSearchInput(BaseModel):
    query: str = Field(description="用于查找信息的搜索查询")

实现网页搜索工具

我们现在定义一个Tool子类,执行针对 DuckDuckGo API 的搜索。让我们按照接下来的列表描述,确定工具类结构形状及其async委托的形状:

class SimpleWebSearchTool(BaseTool):
    name: str = "web_search"
    description: str = "搜索网络获取当前信息。当你需要训练数据中没有的最新数据或事实时使用此项。"
    args_schema: Type[BaseModel] = WebSearchInput

    async def _arun(self, query: str) -> str:
        # 异步实现逻辑
        pass

    def _run(self, query: str) -> str:
        # 同步实现逻辑
        pass
  • name: 智能体调用工具时使用的标识

  • description: 智能体决定何时使用该工具的说明

  • args_schema: 定义输入格式的类

  • _arun: 异步执行方法

  • _run: 同步执行方法

现在我们定义一个针对 DuckDuckGo API 执行搜索的Tool子类。让我们按照下面的描述,确定工具类结构形状及其async委托:

    def _run(self, query: str) -> str:
        try:
            query = query.strip().replace(" ", "+")
            url = f"https://api.duckduckgo.com/?q={query}&format=json&no_html=1&disambig=1"
            response = requests.get(url, timeout=10)
            data = response.json()

            if data.get("AbstractText"):
                return (
                    f"搜索结果({query}):\n"
                    f"{data['AbstractText']}\n\n"
                    f"Source: {data.get('AbstractURL', 'DuckDuckGo')}"
                )
            elif data.get("RelatedTopics"):
                topic = data["RelatedTopics"][0]
                if isinstance(topic, dict) and topic.get("Text"):
                    return f"关于 '{query}' 的相关信息:\n{topic['Text']}"
            return f"未找到关于 '{query}' 的直接答案。"

        except requests.exceptions.Timeout:
            return "请求超时。"
        except requests.exceptions.RequestException as e:
            return f"请求错误:{e}"
        except Exception as e:
            return f"发生未知错误:{e}"

_run方法执行了搜索逻辑。try块处理了“快乐路径”(正常情况),except分支处理了异常。向内看,这里是请求准备的过程:

  1. 清洗查询并将空格替换为+。

  2. 构建 URL。

  3. 发送 GET 请求,并设置 10 秒超时防止挂起。

  4. 将响应解析为 JSON 字典。

有了 JSON 负载后,工具会选择最具信息的字段。工具检查了几个字段:

            if data.get("AbstractText"):
                return (
                    f"搜索结果({query}):\n"
                    f"{data['AbstractText']}\n\n"
                    f"Source: {data.get('AbstractURL', 'DuckDuckGo')}"
                )

此块处理了 DuckDuckGo API 响应包含AbstractText的情况,这通常是一个简短的直接解释。括号内的多行字符串避免了杂乱的转义符。第一行用查询标记结果,最后一行引用来源。

下一个elif处理了存在Definition字段的情况:

            elif data.get("Definition"):
                return (
                    f"定义({query}):\n"
                    f"{data['Definition']}\n\n"
                    f"Source: {data.get('DefinitionURL', 'DuckDuckGo')}"
                )

这与摘要的想法相同,但专门用于字典形式的定义。格式完全一致,只有键和标签不同。https://api.duckduckgo.com/?q={query}&format=json&no_html=1&disambig=1

最后是RelatedTopics情况:

            elif data.get("RelatedTopics") and len(data["RelatedTopics"]) > 0:
                topic = data["RelatedTopics"][0]
                if isinstance(topic, dict) and topic.get("Text"):
                    return f"关于 '{query}' 的相关信息:\n{topic['Text']}"

这些共同确保了用户能够以可读的格式获得清晰的答案和引用。

如果既没有摘要也没有定义,工具将回退到related topics(相关主题):

  • 从列表中选取第一个topic ([0])

确保它是一个包含 Text 字段的字典。将该片段作为答案返回。这样即使没有直接的摘要,也能提供一些回退上下文。工具定义完成后,下一步是测试它是否能对常见查询做出正确响应。

测试网络搜索工具

在此阶段,我们初始化工具并针对几个示例查询运行它,以验证其是否如下列表所示按预期工作:

# Test the web search tool

web_search_tool = SimpleWebSearchTool()

print("🌐 Web Search Tool created successfully!")
  • SimpleWebSearchTool() 使用预定义的名称、描述和args架构构建工具。

  • print确认信息在演示期间很有帮助,用于初始化成功。

实例准备就绪后,我们可以准备一些真实的输入来测试不同的答案类型。

接下来是准备测试搜索。我们收集了一个较小且多样化的列表,以探测摘要、事实和定义,如下列表所示:

### Test searches

print("\n🧪 Testing Web Search Tool:")

test_searches = [
    "Python programming language",
    "current population Tokyo",
    "photosynthesis definition"
]
  • 这种组合针对常见的响应模式:通用摘要、当前事实和类字典定义。

  • 短短列表保持了演示的快速,同时展示了多样性。

现在输入已就绪,我们终于准备好运行搜索循环。我们遍历查询,执行工具,并打印一个紧凑的预览:

for search_query in test_searches:
    print(f"\n Searching: {search_query}")
    result = web_search_tool._run(search_query)
    print(f" Result: {result[:200]}...")

该循环宣布每个查询,调用工具的同步_run方法,并只打印前约200个字符以提高可读性。这是一个快速且实用的检查,验证了网络访问、JSON解析和格式化逻辑,而不会让控制台崩溃。这个测试循环展示了工具如何从网络返回有用信息的片段。每个查询都会经过清理,通过DuckDuckGo API,并打印结果的第一部分供检查。

搜索工具就绪后,下一阶段将其与计算器结合,形成一个丰富的智能体。

第6步:创建一个具有多个工具的研究智能体

现在我们既有了计算器又有网络搜索工具,接下来的逻辑步骤是将它们集成到单一智能体中。这个研究智能体能够在同一个推理循环中执行计算并获取当前信息。

定义研究智能体类

class ResearchAgent:
    """可以搜索网络并执行计算的高级研究智能体"""
    def __init__(self, llm):
        self.llm = llm
        self.tools = [
            web_search_tool,
            calculator_tool
        ]
        self.agent = create_react_agent(llm, self.tools, react_prompt)
        self.agent_executor = AgentExecutor(
            agent=self.agent,
            tools=self.tools,
            verbose=True,
            max_iterations=3,
            max_execution_time=30,
            handle_parsing_errors=True,
            return_intermediate_steps=True
        )
  • self.tools: 可用工具列表(搜索 + 计算器)。

  • create_react_agent: 使用LLM、工具和ReAct提示构建推理智能体。

  • AgentExecutor: 管理带有限制和调试选项的执行。

  • max_iterations=3: 保持研究循环较短。

  • max_execution_time=30: 防止无限制执行。

  • return_intermediate_steps=True: 记录工具调用和输出以保持透明度。

继续后续,我们在新初始化的类中定义了处理单个研究查询和批量处理的方法。

添加研究方法

这些方法将执行器封装为面向用户的函数,用于单个和多次查询:

def research(self, query: str) -> dict:
    """对给定查询进行研究"""
    try:
        result = self.agent_executor.invoke({"input": query})
        return {
            "success": True,
            "query": query,
            "answer": result['output'],
            "error": None
        }
    except Exception as e:
        return {
            "success": False,
            "query": query,
            "answer": None,
            "error": str(e)
        }

research方法执行带有错误处理的单个搜索。它调用agent_executor,捕获输出并返回一个结构化字典,包含成功状态、查询文本、答案以及如果发生任何异常时的错误消息。

def batch_research(self, queries: list) -> list:
    """研究多个查询"""
    results = []
    for query in queries:
        print(f"\n Researching: {query}")
        result = self.research(query)
        results.append(result)
    return results

该方法自动处理多个查询。它遍历每个查询,打印进度,在内部调用research并将结果聚合到字典列表中。这是一个用于批量执行的便捷封装器,同时复用了单查询逻辑。batch_research方法循环多个查询,收集结果并以字典列表的形式返回。

测试研究智能体

我们创建智能体,运行几个查询并观察结果。让我们从准备查询开始:

# Create research agent
research_agent = ResearchAgent(llm)
print("🔬 Research Agent created successfully!")

# Test the research agent
print("\n Testing Research Agent:")
research_queries = [
    "What is the current population of Japan and how has it changed over the last decade?",
    "How does machine learning work and what are its main applications?",
    "What are the environmental benefits of solar energy?"
]

此区块使用语言模型(llm)设置了ResearchAgent。打印一条确认信息,随后是宣布测试的短幅。然后准备research_queries列表,包含三个多样化的查询供智能体处理。

查询准备好后,代码进入执行循环:

for query in research_queries:
    print(f"\n{'='*80}")
    print(f"🔬 Research Query: {query}")
    print('='*80)
    result = research_agent.research(query)
    if result['success']:
        print(f"✅ Research Complete!")
        print(f" Answer: {result['answer']}")
    else:
        print(f"❌ Research Failed: {result['error']}")

此循环遍历每个准备好的查询。为了可读性,在显示查询之前会打印分隔符行。每个查询都会传递给智能体的研究方法。根据执行成功与否,代码将打印带有✅标记的答案或带有❌的错误。

测试演示了组合智能体如何通过搜索实时数据并应用带计算的推理来回答复杂问题。这展示了将多个工具链入一个连贯工作流的潜力。

在确保这一进展后,智能体不再仅仅是一个简单的原型,而是一个不断成长的系统,准备好应对更广泛的挑战。从这里开始,视野将扩展到调度、数据分析以及与外部服务的无缝连接,每个新工具都会将智能体转化为一个更具活力、更有能力的协作者。

第7步:专业化客户服务代理

在此阶段,我们将为技术、账单和通用查询构建特定角色的代理。我们将定义一个可重用的基类,配置其执行器,然后实现具有针对性提示词(prompts)和工具集的专业化子类。

定义基专业化代理

在以下代码中,我们创建了一个可重用的代理包装器,它设置了专业领域、系统提示词和执行策略:

  1. 我们首先定义一个捕获角色、工具和提示默认值的可重用基类。构造函数存储了语言模型、角色标签和工具集。如果没有提供系统提示词,它会自动生成一个与专业领域匹配的提示词,以确保代理在行为和语气上保持一致:
class SpecializedAgent:
    """专业化客户服务代理的基类"""
    def __init__(
        self, llm, specialty: str, tools: list, system_prompt: str = None
    ):
        self.llm = llm
        self.specialty = specialty
        self.tools = tools
        self.system_prompt = system_prompt or f"You are a helpful {specialty} specialist."
  1. 随后,我们创建代理本身,使用ReAct框架将推理与工具使用结合。在这里,智能体绑定了语言模型、它可以使用的工具以及一个引导提示词,该提示词指示如何在推理和行动之间交替:
self.agent = create_react_agent(llm, tools, react_prompt)
  1. 接下来,我们初始化AgentExecutor,这是一个管理智能体运行方式、限制执行时间并优雅处理错误的运行时组件:
self.agent_executor = AgentExecutor(
    agent=self.agent,
    tools=tools,
    verbose=False,
    max_iterations=2,
    max_execution_time=20,
    handle_parsing_errors=True,
    return_intermediate_steps=False
)

这种设置确保了代理的高效运行:它最多只会尝试两次推理动作循环,不会超过20秒,并且在输出需要轻微修正时能够恢复。为了清晰起见,中间步骤被隐藏,为终端用户保持结果的简洁。

基础到位后,我们现在可以定义每个角色如何处理传入的消息。

在角色中处理传入的查询

简单查询通过LLM直接回答;更复杂的查询则使用代理:

  1. 我们首先查看方法本身及其整体结构。该方法路由传入的客户查询。它从一个try块开始,其中对查询进行分析和处理,单个except分支确保能够优雅地捕获任何意外错误,返回回退消息:
def handle(self, query: str) -> str:
    """带有更佳错误处理的客户查询"""
    try:
        # 简单查询与复杂查询的逻辑
        ...
    except Exception as e:
        ...
  1. 接下来是try块中的成功路径。逻辑区分了simple查询(简短且不涉及计算或搜索)和complex查询。简单查询直接传递给语言模型以获得快速答案。复杂查询则路由通过代理执行器,该执行器可以推理并使用工具。如果失败,则返回礼貌的回退消息:
### 对于简单查询,直接尝试LLM输出
if len(query.split()) < 10 and not any(word in query.lower() for word in ['calculate', 'search']):
    return llm.invoke(query)

### 对于复杂查询,使用代理
enhanced_query = f"As a {self.specialty} specialist, help with {query}"
result = self.agent_executor.invoke({"input": enhanced_query})
return result.get('output', 'I apologize, I was unable to process your request.')
  1. 最后,我们查看异常处理。此区块确保了韧性。与其暴露错误,会将客户引导至人工支持。这维护了用户信任:
except Exception as e:
    return "I apologize for the technical difficulty. Please contact our support team."

这种方法让简单问题保持快速且无需工具。

实现特定角色代理

为了构建能够处理不同类型客户查询的代理,我们将实现SpecializedAgent子类。每个代理类都设置了针对性的提示词和定制工具集。我们将逐步完成它们,从技术支持开始,然后是账单,最后是通用查询:

  1. 我们从技术支持代理开始。该类继承自SpecializedAgent并将其角色设置为处理技术问题。它带有一个精心设计的系统提示词,强调故障排除和逐步指导。为了支持这一角色,代理被赋予了web_search_tool的权限,使其能够在推理过程中查找已知问题和解决方案:
class TechnicalSupportAgent(SpecializedAgent):
    """处理技术问题的专业化代理"""
    def __init__(self, llm):
        system_prompt = """你是一个技术支持专家。你帮助客户处理:
            - 软件Bug和错误
            - 安装和设置问题
            - 性能问题
            - 功能使用和配置
            - 故障排除步骤
            尽可能提供清晰的步骤化解决方案。"""
        super().__init__(
            llm=llm,
            specialty="technical support",
            tools=[web_search_tool],  # 查看已知问题和修复
            system_prompt=system_prompt
        )
  1. 账单代理遵循相同的结构,但将其角色转向支付和订阅问题。它的系统提示词鼓励同理和清晰的下一步,而工具集切换到calculator以处理退款和比例计算:
class BillingSupportAgent(SpecializedAgent):
    """处理账单和支付问题的专业化代理"""
    def __init__(self, llm):
        system_prompt = """你是一个账单支持专家。你帮助客户处理:
            - 支付处理问题
            - 订阅管理
            - 退款申请
            - 发票问题
            - 价格咨询
            始终保持同理心并提供清晰的下一步。"""
        super().__init__(
            llm=llm,
            specialty="billing support",
            tools=[calculator_tool],  # 计算退款或比例
            system_prompt=system_prompt
        )
  1. 最后,通用代理将处理不属于前两个角色的其他问题,利用整个工具集,并具备更开放式的系统提示词。
class GeneralSupportAgent(SpecializedAgent):
    """处理通用查询的专业化代理"""
    def __init__(self, llm):
        system_prompt = """你是一个通用客户支持专家。你帮助客户处理:
            - 一般性问题
            - 产品信息查询
            - 账户管理
            - 合作伙伴或销售咨询
            保持专业、友好,并提供有用的信息。"""
        super().__init__(
            llm=llm,
            specialty="general support",
            tools=[web_search_tool, calculator_tool],
            system_prompt=system_prompt
        )

最后,通用支持代理采用相同的基础模式来处理如账户问题和产品信息等广泛的查询。它的回答旨在简洁且友好,并且它再次使用网络搜索工具来提供最新的通用性解答:

class GeneralSupportAgent(SpecializedAgent):
    """用于通用查询的专业化代理"""

    def __init__(self, llm):
        system_prompt = """你是一个通用的客户支持专家。你帮助客户处理:

            - 通用产品信息
            - 账户问题
            - 政策说明
            - 基础操作指南

            保持回答简洁友好。"""

        super().__init__(
            llm=llm,
            specialty="general customer support",
            tools=[web_search_tool],  # 搜索通用信息
            system_prompt=system_prompt
        )

每个子类都定义了自己的系统提示词,在塑造语气和范围的同时,选择了与其领域相匹配的工具——技术和通用角色使用搜索工具,账单角色使用计算器。尽管存在这些差异,它们都继承了相同的处理方法,确保了逻辑的一致性,并为编排多代理系统做好了准备。

现在专业化代理库已经就绪,是时候将它们整合在一起了。通过协调它们互补的优势,我们可以创建一个协同工作的多代理系统,能够处理多样化的客户需求。

步骤 8:多代理客户服务系统

现在,我们将专业代理组装一个统一的服务。CustomerServiceSystem 类初始化每个角色,对输入的查询进行分类,将其路由到正确的代理,并返回结构化结果:

  1. 我们首先创建系统并初始化代理。我们构建了一个控制器,它拥有每个特定角色代理的实例。这个初始化器让系统能够在无需在调用代码中额外配置的情况下,处理技术、账单和通用查询:
class CustomerServiceSystem:
    """带有多个专业化代理的智能客户服务系统"""

    def __init__(self, llm):
        self.llm = llm
        self.technical_agent = TechnicalSupportAgent(llm)
        self.billing_agent = BillingSupportAgent(llm)
        self.general_agent = GeneralSupportAgent(llm)
  1. 然后我们分类应该由哪个代理进行响应。一个简短的 LLM 提示词使用exactly one word指令将查询映射到 TECHNICAL、BILLING 或 GENERAL:
def route_inquiry(self, inquiry: str) -> str:
    """使用直接调用 LLM 确定哪个代理应该处理查询"""
    routing_prompt = f"""分类此客户查询。仅回答一个单词:TECHNICAL, BILLING, 或 GENERAL。

    TECHNICAL:应用崩溃、漏洞、错误、安装、性能、登录
    BILLING:支付、订阅、退款、发票、定价
    GENERAL:信息、账户问题、政策、操作指南

    查询:"{inquiry}"
    分类结果:"""

    try:
        result = self.llm.invoke(routing_prompt)
        classification = result.content.strip().upper()

        if "TECHNICAL" in classification:
            return "TECHNICAL"
        elif "BILLING" in classification:
            return "BILLING"
        else:
            return "GENERAL"
    except Exception as e:
        print(f"路由失败:{e}, 默认为 GENERAL")
        return "GENERAL"

这个简单的分类器保持了路由的轻量化,避免了不必要的工具调用,同时便于日后扩展更多类别。

  1. 一旦分类完成,系统就会调用相应的代理并返回一个结构化记录:
def handle_inquiry(self, inquiry: str) -> dict:
    """通过适当代理处理客户查询"""
    route = self.route_inquiry(inquiry)

    if route == "TECHNICAL":
        agent_name = "Technical Support"
        response = self.technical_agent.handle(inquiry)
    elif route == "BILLING":
        agent_name = "Billing Support"
        response = self.billing_agent.handle(inquiry)
    else:
        agent_name = "General Support"
        response = self.general_agent.handle(inquiry)

    return {
        "inquiry": inquiry,
        "routed_to": route,
        "agent_name": agent_name,
        "response": response
    }

返回的字典可以直观地显示哪个代理回答了问题以及原因。

  1. 一个小方法处理查询列表并返回一个列表。这个辅助方法支持批量操作:
def batch_handle(self, inquiries: list) -> list:
    """处理多个查询"""
    results = []

    for inquiry in inquiries:
        result = self.handle_inquiry(inquiry)
        results.append(result)

    return results
  1. 最后,我们创建系统并端到端运行几个示例以观察:
cs_system = CustomerServiceSystem(llm)
print("🎯 多代理客户服务系统创建成功!")

test_inquiries = [
    "应用启动时崩溃",
    "需要上个月的退款",
    "你们的营业时间是时候?",
    "重置密码帮助"
]

for inquiry in test_inquiries:
    print(f"\n{'='*80}")
    print(f"🎫 客户查询:{inquiry}")
    print('='*80)
    result = cs_system.handle_inquiry(inquiry)
    print(f"🎯 路由至:{result['agent_name']} ({result['routed_to']})")
    print(f" 回答:{result['response']}")

这些测试验证了整个流程:分类、路由和响应。从这里开始,你可以添加更多专业、扩展路由逻辑或集成真实操作的 API。

步骤 9:交互式演示界面设置

在交互式测试编排好的代理之前,我们需要引入一个轻量级界面(输入、下拉菜单和按钮)来直接测试我们的代理。这让我们能够在 notebook 中关注代理的行为。

导入 UI 组件

为了实现这一点,我们将 IPython 的显示工具与 ipywidgets 结合,从而获得了从按钮、下拉菜单到可以内联渲染的输出面板和 HTML 代码段的所有组件:

from IPython.display import display, clear_output, HTML
import ipywidgets as widgets

这些导入为交互元素和辅助工具提供了基础组件,稍后会将代理响应连接到可见的笔记本输出。

创建界面类

我们将 UI 逻辑封装在一个专门的类中。这种方法使得职责清晰。组件的创建、事件绑定和渲染整齐地组织在一个地方,便于日后复用或扩展:

class AgentDemoInterface:
    """使用 Jupyter widgets 的代理交互式演示界面"""

    def __init__(self):
        # 存储所有代理的引用
        self.calculator_agent = calculator_agent
        self.research_agent = research_agent
        self.cs_system = cs_system

        # 创建界面小组件
        self.create_widgets()
        self.setup_event_handlers()
        # 显示界面
        self.display_interface()

构造函数锚定了现有智能体(agents)的引用,然后立即构建小组件、绑定处理程序并在笔记本中展示界面。

通过将所有内容集中在一个类中,该演示界面就像一个控制面板。它允许我们在 Colab 环境中切换智能体、输入查询并并排查看答案,而无需额外的设置。

创建小组件

我们现在定义标题、智能体选择器、查询区域、按钮、输出容器以及用于快速测试的示例按钮:

  1. 我们首先创建一个标题,以便让界面能够立即传达意图和背景:
self.title = widgets.HTML(
    value="<h2>🤖 多智能体 AI 系统演示</h2><p>测试我们在本次工作坊中构建的所有智能体!</p>",
    layout=widgets.Layout(margin='0 0 20px 0')
)

这个 HTML 小组件渲染了一个标题和一个带有额外下边距的短副标题。它在视觉上锚定了 UI,并为演示设定了统一的基调。清晰的标题立即建立了背景,使界面感觉是有目的的、精良的并且准备好进行实践实验。

  1. 接下来,智能体选择器允许我们在计算器、研究和客户服务功能之间轻松切换:
self.agent_selector = widgets.Dropdown(
    options=[
        ('🧮 计算器智能体', 'calculator'),
        ('🔬 研究智能体', 'research'),
        ('🎯 客户服务系统', 'customer_service')
    ],
    value='calculator',
    description='选择智能体:',
    style={'description_width': '120px'},
    layout=widgets.Layout(width='400px')
)

这个 dropdown 将可读标签映射到内部值,设置了默认值并设置了描述和控件的大小。它是主要的路由开关。单一的描述性选择器减少了摩擦,引导了流程并防止对哪个智能体将处理请求产生困惑。

  1. 然后我们添加了一个多行文本区域,以便方便地输入或粘贴查询:
self.query_input = widgets.Textarea(
    value='What is 25 * 47 + 138?',
    placeholder='在此输入您的查询...',
    description='您的查询:',
    layout=widgets.Layout(width='100%', height='100px'),
    style={'description_width': '120px'}
)

text area 为较长的提示(prompt)提供了空间,具有合理的默认值、占位符引导以及与其他控件对齐的一致标签宽度。宽大的输入框鼓励实验,容纳更长的问题,并保持了 UI 的整洁和可预测。

  1. Submit 按钮触发使用所选智能体处理当前提示:
self.submit_button = widgets.Button(
    description='🚀 提交查询',
    button_style='primary',
    layout=widgets.Layout(width='200px', height='40px')
)

button 使用引人注目的样式和固定尺寸来清晰地传达其主要操作,并在不同布局中保持稳定。醒目的行动号召引导交互,指明了点击位置,并使工作流一目了然,以便快速测试。

  1. 一个 Clear 按钮重置输出区域,帮助迭代运行保持可读性:
self.clear_button = widgets.Button(
    description='🗑️ 清空输出',
    button_style='warning',
    layout=widgets.Layout(width='150px', height='40px')
)

独特的视觉样式将此控件与提交操作区分开,而固定尺寸维持了整体的对齐和平衡。轻松清空输出支持快速循环,防止混乱并使每次运行的结果集中且可见。

随后,输出容器显示带有边框、内边距和针对长内容的滚动条响应:

self.output_area = widgets.Output(
    layout=widgets.Layout(
        border='1px solid #ccc',
        padding='10px',
        margin='10px 0',
        height='400px',
        overflow='auto'
    )
)

带边框和内边距的框将结果与控件隔离开;固定的高度和滚动行为在较长的会话期间保持布局紧凑。良好的输出区域框了可读性,并在响应增长时保留了结构,确保界面保持冷静和井然有序。

  1. 我们还为每个智能体定义了示例提示,使得测试运行触手可及:
self.examples = {
    'calculator': [
        'What is 25 * 47 + 138?',
        'Calculate 15% of 250',
        'If I have $1000 and spend $347, how much is left?'
    ],
    'research': [
        'What is the current population of Tokyo?',
        'How does solar energy work?',
        'What are the benefits of electric vehicles?'
    ],
    'customer_service': [
        'My app keeps crashing when I upload files',
        'I was charged twice for my subscription',
        'How do I change my password?'
    ]
}

精心挑选示例展示了每个智能体的典型用例,使得无需构思提示即可轻松探索。预填充的示例加速了发现,揭示了每个智能体的优势,并为快速健康检查提供了可靠的基准。

  1. 最后,我们生成了点击时加载这些示例的按钮。每个示例都获得了一个带有信息风格的小按钮,点击时会将文本注入输入框。可点击的示例消除了摩擦,鼓励快速迭代,并允许在跨智能体之间一致地比较输出:
self.example_buttons = {}

for agent_type, examples in self.examples.items():
    buttons = []
    for i, example in enumerate(examples):
        btn = widgets.Button(
            description=f"示例 {i+1}",
            button_style='info',
            layout=widgets.Layout(width='100px', margin='2px')
        )
        btn.example_text = example
        btn.on_click(self.load_example)
        buttons.append(btn)
    self.example_buttons[agent_type] = buttons

该方法整合了协调的面板、标识、路由、输入、操作和示例,使得实验流畅、可重复且视觉有序,便于对智能体行为进行评估。

构建聊天工作流

为了完成应用程序的用户端,我们需要将界面与模型连接起来。这不仅仅是发送查询,它需要连接事件、渲染界面、处理响应,并通过加载状态和清除输入保持良好的用户体验。

在这一部分中,我们将这些组件组合成一个驱动聊天的连续工作流:

  1. 我们首先将用户操作(如按钮点击和下拉菜单更改)连接到相应的处理函数,以便界面能够动态响应。这些绑定将用户操作引导到相应的方法——submit 处理,clear 重置输出,agent_change 刷新示例按钮和默认文本:
def setup_event_handlers(self):
    """设置小组件的事件处理器"""
    self.submit_button.on_click(self.handle_query)
    self.clear_button.on_click(self.clear_output)
    self.agent_selector.observe(self.on_agent_change, names='value')
  1. 然后我们将控件和输出按垂直堆叠,并渲染完整界面,使其直接显示在笔记本中,如下文列表所示:
def display_interface(self):
    """显示完整界面"""
    # 控制面板
    controls = widgets.VBox([
        self.title,
        self.agent_selector,
        widgets.HTML("<b>Quick Examples:</b>"),
        widgets.HBox(
            self.example_buttons[self.agent_selector.value]),
        self.query_input,
        widgets.HBox([self.submit_button, self.clear_button])
    ])

    # Main interface
    main_interface = widgets.VBox([
        controls,
        widgets.HTML("<b>Output:</b>"),
        self.output_area
    ])

    display(main_interface)
  • widgets.VBox([...]) 将控件垂直堆叠;内层的 HBox 将示例按钮和操作按钮分组。

  • display(main_interface) 在当前 notebook 单元格中渲染复合 UI。

现在 UI 已经可见,我们将添加用于智能体切换、加载示例、清除输出和处理查询的行为。

  1. 当切换选定的智能体时,界面会更新其示例按钮并自动将匹配的示例查询加载到输入字段中。这确保了界面与选定的智能体同步,立即为用户提供相关的示例提示词:
def on_agent_change(self, change):
    """Handle agent selection change"""
    new_agent = change['new']
    # 更新示例按钮
    for widget in self.example_buttons[new_agent]:
        widget.layout.display = 'block'
    # 根据智能体更新示例文本
    examples = self.examples[new_agent]
    if examples:
        self.query_input.value = examples[0]
  1. 点击示例按钮将将其预定义的提示词直接放入输入字段,使其无需手动输入即可测试查询。这是一个小小的便利功能,可以加速对不同智能体场景的尝试,而无需手动打字:
def load_example(self, button):
    """Load example query when button is clicked"""
    self.query_input.value = button.example_text
  1. 该方法随后会清除输出显示,为下一次交互提供整洁的状态,并保持界面易于追踪。使用 clear_output() 可以保持运行之间的显示整洁,特别是在演示期间:
def clear_output(self, button):
    """Clear the output area"""
    with self.output_area:
        clear_output()

处理查询提交

此方法获取激活的智能体和当前查询,将请求发送到相应的后端,然后以结构化、可读的格式显示响应:

  1. 我们从方法签名开始为处理来自界面的提交查询建立入口:
def handle_query(self, button):
    """Handle query submission"""

此方法绑定到 Submit 按钮上。点击时,它将编排清除显示、验证输入、按智能体类型路由以及打印结果或错误。

  1. 现在,我们将输出范围限制在组件,清除之前的内容,并准备待处理的输入:
with self.output_area:
    clear_output()
    agent_type = self.agent_selector.value
    query = self.query_input.value.strip()
    if not query:
        print("❌ Please enter a query")
        return
    print(f"🤖 Processing with {self.agent_selector.label} agent...")
    print(f" Query: {query}")
    print("=" * 60)

with 上下文将所有打印内容发送到输出组件,clear_output() 擦除旧结果,读取并验证输入,短标题会显示哪个智能体将处理查询。

  1. 接下来是 try/except 骨架,它将正常执行路径与错误处理分开:
try:
    # route by agent type and print structured results
    ...
except Exception as e:
    # print a helpful error and guidance
    ...

try 块包含路由和打印逻辑;except 块提供了优雅的回退机制,确保 UI 不会崩溃,并且用户可以看到清晰的后续步骤。

  1. 在 try 块内部,我们首先处理计算器分支——调用智能体并打印其输出:
if agent_type == 'calculator':
    result = self.calculator_agent.invoke({"input": query})
    print("🧮 Calculator Result:")
    print(f"✅ {result.get('output', 'Calculation completed')}")

此路径将查询发送到计算智能体的 invoke 方法,并为数学或简单计算打印带标签的简洁结果。

  1. research 分支委托给 research_agent.research(query) 并打印答案或显式错误:
elif agent_type == 'research':
    result = self.research_agent.research(query)
    if result['success']:
        print("🔬 Research Complete:")
        print(f"📋 {result['answer']}")
    else:
        print(f"❌ Research Error: {result['error']}")

在这里,辅助方法返回一个结构化字典。成功打印答案;失败则显示捕获的错误消息而不抛出异常。

  1. customer_service 分支调用多智能体系统,然后显示路由信息和最终响应:
elif agent_type == 'customer_service':
    result = self.cs_system.handle_inquiry(query)
    print("🎯 Customer Service Response:")
    print(f"📍 Routed to: {result['agent_name']}")
    print(f" Response: {result['response']}")

此路径揭示了哪个智能体处理了查询并显示其消息,提供了路由决策的透明度。

  1. 在任何分支完成后,一个结束标记会确认成功并在视觉上区分运行过程:
print("\n" + "=" * 60)
print("✅ Query completed!")

这些最后的打印增加了分割符和完成说明,使得多次运行更容易扫描。

  1. 最后,异常处理器会将意外错误转换为友好的指导:
except Exception as e:
    print(f"❌ Unexpected error: {str(e)}")
    print("Please try again or contact support.")

任何意外错误都会被捕获、总结,并提供清晰的后续步骤,保持界面的韧性和以用户为中心。

  1. 最后的消息确认类已定义并可以实例化:
print("🎨 Interactive demo interface created successfully!")

这很好地完成了准备工作,下一步可以专注于演示。

第 10 步:启动交互演示

界面定义后,我们现在可以在 notebook 中启动实时演示。此步骤将打印简单的使用说明并创建界面,界面通过其 display_interface() 方法自动渲染。

使用说明

为新用户提供了一个简要清单,提供了如何有效地与演示交互的清晰指南:

# 显示使用说明
print("🚀 Launching Interactive Multi-Agent Demo Interface!")
print("\nℹ️  How to use:")
print("1. 从下拉菜单中选择智能体")
print("2. 点击示例按钮或输入自己的查询")
print("3. 点击 'Submit Query' 查看智能体的运行情况")
print("4. 使用不同类型的问题尝试不同的智能体")
print("\n" + "="*60)

这些指令将指导并在并在工作坊期间减少困惑。

创建并显示界面

我们创建 AgentDemoInterface 的实例,将其与我们的智能体关联,并允许界面在 notebook 内部构建并显示自身:

# 创建并显示演示界面
demo_interface = AgentDemoInterface()

这一行代码将整个系统整合到一个可用的 UI 中。你可以选择一个智能体,尝试示例,并观察输出流式进入面板。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_07.png

图 5.7:代理测试 UI 界面

界面已就绪。不要只读代码——去使用它。选择一个智能体,输入查询并点击 Submit Query。尝试示例或编写自己的问题。观察每个智能体如何做出不同的响应。这种实践将理论转化为具体的、交互式的体验。

第 11 步:代理性能测试

现在智能体和 UI 均运行正常,是时候衡量它们在真实负载下的表现了。这一步提供了一个脚手架性能框架,用于对响应进行计时、统计成功/失败次数并汇总结果,便于比较不同的智能体并识别优化机会。

定义性能测试类

测试逻辑被组织在一个专门的类中,在这里我们以脚手架的形式概述 test_agent_performance 方法,以突出其流程,而不被过多细节淹没:

import time
from datetime import datetime

class AgentPerformanceTester:
    """测试并衡量代理性能"""
    def __init__(self):
        self.test_results = []

    def test_agent_performance(self, agent_name: str, agent_func, test_queries: list):
        """对智能体运行多次查询并收集计时与成功率指标"""
        # 1) 初始化结果结构
        # 2) 遍历测试查询
        #    - 开始计时
        #    - 尝试调用智能体
        #    - 成功:计算已用时间,更新成功计数器,构建 query_result
        #    - 失败:计算已用时间,更新失败计数器,构建 query_result
        #    - 将 query_result 添加到结果中
        # 3) 计算最终平均值 / 成功率
        # 4) 持久化到 self.test_results 并打印摘要
        # 5) 返回结果
        ...

该脚手架概述了控制流,同时省略了特定的 print 语句和微细节,使注意力集中在你将衡量和报告的核心机制上。

结果初始化

该方法通过设置 results 字典开始,建立计数器和存储字段,这些将在整个性能测试过程中跟踪查询结果、用时间和整体成功指标:

results = {
    'agent_name': agent_name,
    'total_queries': len(test_queries),
    'successful_queries': 0,
    'failed_queries': 0,
    'total_time': 0,
    'average_time': 0,
    'query_results': []
}

此字典积累了整个运行过程指标和每个查询的记录。它跟踪计数、总/平均时间以及一个用于粒度分析或后期导出的 query_results 列表。

所有测试循环

方法继续对所有测试查询进行循环,对每次尝试进行计时,处理成功和错误情况,并将结构化记录添加到 results 列表中以保持一致性:

for i, query in enumerate(test_queries, 1):
    # 🕒 开始测量时间
    start_time = time.time()
    try:
        # ▶️ 尝试运行智能体调用
        #    例如,result = agent_func(query)
        # ⏱️ 成功后测量已用时间
        ...
    except Exception as e:
        # ⏱️ 错误时测量已用时间
        ...
    # 📦 添加每个查询记录
    results['query_results'].append(query_result)

循环对每次尝试进行计时,分离成功和错误路径,并确保每个查询都生成一个一致的 query_result 条目并添加到 results['query_results']。

处理计时的智能体调用(成功路径)

在成功路径中,捕获智能体的输出,测量执行时间并更新计数器。会存储一个结构化记录,包括 result_length,以帮助检测异常短或过长的响应:

result = agent_func(query)
end_time = time.time()
execution_time = end_time - start_time
results['successful_queries'] += 1
results['total_time'] += execution_time
query_result = {
    'query': query,
    'success': True,
    'execution_time': execution_time,
    'result_length': len(str(result))
}

成功后,我们计算已用时间,增加成功计数器并存储紧凑的每个查询记录。跟踪 result_length 对于发现极短或意外的输出很有用。

处理错误(失败路径)

在失败路径中,仍然测量执行时间,增加失败计数器并捕获异常消息。这确保即使在发生错误时,每个查询也能生成一致的记录:

end_time = time.time()
execution_time = end_time - start_time
results['failed_queries'] += 1
results['total_time'] += execution_time
query_result = {
    'query': query,
    'success': False,
    'execution_time': execution_time,
    'error': str(e)
}

在'error': str(e)中,异常信息被转换为字符串并存储在'error'键中。

错误仍然产生有效记录:我们对其计时,增加失败计数器并存储异常文本。

最终指标计算与持久化

在结束时,计算平均值和成功率。结果被存储,打印摘要,框架已准备好进行测试:

if results['total_queries'] > 0:
    results['average_time'] = results['total_time'] / results['total_queries']
    results['success_rate'] = results['successful_queries'] / results['total_queries']
self.test_results.append(results)
self.print_performance_summary(results)

平均延迟和成功率汇总了运行情况。我们随后持久化results对象并输出摘要。有了这个基准框架,你可以完善提示词、调整工具或添加缓存。然后重新运行测试以确认改进。

方法:打印指标摘要

此辅助函数在运行完成后打印简洁报告:

def print_performance_summary(self, results):
    """打印性能摘要"""
    print(f"\n📊 {results['agent_name']} 的性能摘要:")
    print(f"   总查询数: {results['total_queries']}")
    print(f"   成功: {results['successful_queries']}")
    print(f"   失败: {results['failed_queries']}")
    print(f"   成功率: {results.get('success_rate', 0):.1%}")
    print(f"   平均时间: {results['average_time']:.2f}s")
    print(f"   总时间: {results['total_time']:.2f}s")

此报告显示了关键 KPI:吞吐量、可靠性和延迟。你以后可以将其扩展为导出 CSV/JSON。

创建测试器并定义特定的代理函数

我们实例化测试器,定义代表性查询,并将每个智能体调用封装在符合agent_func(query)签名的小函数中:

# 创建性能测试器
tester = AgentPerformanceTester()

# 测试每个智能体
print("🔬 开始全面的代理性能测试...")

# 测试计算器智能体
calc_queries = [
    "15 + 25 等于?",
    "计算 12 * 8",
    "100 / 4 等于?"
]

def test_calculator(query):
    return calculator_agent.invoke({"input": query})['output']
  • tester = AgentPerformanceTester(): 初始化测试框架和结果存储器

  • calc_queries: 小型算术测试用例

  • test_calculator(query): 将计算器代理适配到预期的函数签名并提取'output'字段

在准备好计算路径后,我们可以添加研究和客户服务的封装器。

研究代理封装与查询

该封装器在成功时返回答案,并在研究代理报告错误时抛出异常:

# Test Research Agent
research_queries = [
    "什么是人工智能?",
    "再生能源的益处"
]

def test_research(query):
    result = research_agent.research(query)
    if result['success']:
        return result['answer']
    else:
        raise Exception(result['error'])

该封装器执行快速失败策略,以确保错误被记录并计数,从而生成真实的可靠性指标。

客户服务系统封装与查询

我们将查询路由到客户服务系统,并返回人类可读的响应:

# Test Customer Service System
cs_queries = [
    "应用程序启动时崩溃",
    "需要退款帮助"
]

def test_customer_service(query):
    result = cs_system.handle_inquiry(query)
    return result['response']

这个小型适配器隔离了结果提取的方式,而无需更改测试器的核心逻辑。

执行性能运行

我们将每个封装器及其对应的查询集传递给测试器,测试器将按照列表所示打印实时进度和每个代理的摘要:

tester.test_agent_performance("Calculator Agent", test_calculator, calc_queries)
tester.test_agent_performance("Research Agent", test_research, research_queries)
tester.test_agent_performance("Customer Service", test_customer_service, cs_queries)
  • 每次调用都会触发计时、日志记录和最终的 KPI 总结

  • 结果将附加到tester.test_results中供稍后检查

在此阶段,我们的代理已经完成了设计、编排并配备了交互式界面。最后一步是性能测试,成功率和平均响应时间等指标将揭示个体的有效性和整个系统的行为。这些基准测试凸显了优势,暴露了劣势,并为迭代改进提供了基础。通过对提示词、新工具或缓存策略的完善,系统可以稳定地变得更加可靠和响应,且每个周期都会根据这些基准进行验证。

在整个研讨会上,我们从基础组件进展到动态多代理框架。在此过程中,我们集成了工具,创建了专用代理并构建了交互式演示,确保理论通过动手实践得到强化。到结束时,参与者可以直接对代理进行实验,比较它们的输出并衡量跨任务的性能。这种设计、执行和评估的结合展示了简单的原型如何成长可扩展的生产级系统,同时也指向了创意扩展和现实应用。

研讨会 2:使用 n8n 构建视觉 AI 代理

有时,从头编写解决方案并不是选择——太慢、太复杂,或者简单不值得投入这些成本。这就是 n8n 发挥作用的地方,它让我们能够快速构建既快速又高效的工作流。在本次研讨中,我们将把金融数据、新闻源和 Mistral AI 连接到一个自动化流水线中,最后直接向 TG 发送个性化交易见解。结束时,你将看到复杂的想法如何快速转变为可行的解决方案。

是时候做好准备了。我们将从安装和准备 n8n 开始,然后将其连接到几个为我们的工作流提供支持的免费数据源。这些基础将提供所需的输入(符号、价格历史和新闻),后续步骤将对其进行增强和分析。

第 1 :设置 n8n 和免费数据源

在起始步骤中,我们为工作流奠定了坚实的基础:一致的 n8n 环境和可以免费访问的数据流。你将使用 Docker 启动 n8n,注册实例,浏览界面并准备第一个手动触发(Manual Trigger)节点。通过此,画布将成为我们后续 AI 增强的乐场。让我们开始吧:

  1. 我们将在本地启动 n8n,以在机器间建立一个干净、可重复的基准:
# 使用 Docker 启动 n8n
docker run -it --rm --name n8n -p 5678:5678 -v ~/.n8n:/home/node/.n8n docker.n8n.io/n8nio/n8n

运行此操作后,本地工作空间将启动,准备好连接你的第一个工作流。此步骤解锁了编辑器,并确保你可以放心地保存、执行和管理工作流。

  1. 对于首次使用的用户,n8n 注册屏幕会显示带有Get started(开始使用)按钮的欢迎页面。点击后,你将看到创建所有者账户的表单:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_08.png

图 5.8:n8n 注册界面

注册完成后,你将立即进入带有空白画布的主工作空间:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_09.png

图 5.9:n8n 主工作空间

  1. 现在找到并添加Manual Trigger(手动触发)节点,这是我们工作流测试的关键起点。记住,可以在右侧的面板中搜索任何节点,然后简单地将其拖到画布上,这样即使是组装复杂的流程也变得快速:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_10.png

图 5.10:节点选择搜索框

你现在有了一个运行中的 n8n、一个已注册的工作空间,并知道如何添加第一个触发节点。界面看起来应该很熟悉,画布已准备就绪。这个基础让我们能够安全地迭代——没有谜团,只有清晰。很棒的开始!n8n 工具有了心跳,有了成长的空间。

接下来,我们将为 TG 准备友好的通知,创建一个接收见察的机器人和群聊。

步骤 2:配置用于的 TG 机器人和聊天

在这一步中,我们将自动化与人连接起来。你将创建一个 TG 机器人,获取其访问令牌(access token),设置群聊并获取聊天 ID。这为你你的工作流最终发布见察的通道做好准备,使自动化可见且具有协作性。我们从与@BotFather的 TG 通信开始。

TG 支持 iPhone、Android、Windows 和 macOS,因此你可以在任何喜欢的设备上使用它。如果你还没有账户,注册非常快速且免费。只需从TG.org/apps下载应用并按照屏幕上的步骤操作。几分钟后你就可以与@BotFather聊天了。

过程从@BotFather开始,它是 TG 官方的机器人创建工具。打开 TG,搜索@BotFather并与其开始聊天。然后按照以下步骤操作:

  1. 通过输入/newbot创建新机器人,在提示时为你的机器人提供一个name(这是用户看到的名称)。选择一个以"bot"结尾的username(例如MistralFinancialInsightBot)。@BotFather将回复一个unique API token(唯一 API 令牌)。复制并安全地存储它。此令牌用于从代码中验证并连接你的机器人:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_11.png

图 5.11:创建机器人

正如图 5.11所示,@BotFather返回了基本的访问令牌,你之后需要它来授权 n8n。

此时,请务必安全地存储机器人令牌。它是允许你的工作流可靠地发布消息的关键。

  1. 接下来,你把我们的新机器人邀请到结果交付的群聊中。这可以是合作者小组,也可以是你自己的测试空间。图 5.12展示了你的机器人被成功加入群组的时刻,确认它已准备好转发消息:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_12.jpg

图 5.12:机器人已加入群组

  1. 为了传递消息,n8n 必须确切知道在哪里发布(https://api.TG.org/bot%3cBOT_TOKEN%3e/getUpdates)。我们将查询 TG 的 API 来提取数字聊天标识符。URL 模板位于下方https://api.TG.org/bot<BOT_TOKEN>/getUpdates。你需要将 URL 的该部分替换为你从@BotFather收到的自己的访问令牌。然后,只需将其粘贴到浏览器地址栏中。

图 5.13描述了 JSON 响应,其中嵌入了 chat ID。此值将你的工作流连接到正确的对话:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_13.png

图 5.13:通过 API 获取聊天 ID

  1. 接下来,滚动 JSON 直到找到"chat"对象并记录其"id"字段的值。在图 5.14中,你会注意到聊天 ID 在负载中被清晰地高亮,方便之后将其复制到 n8n 中。

使用VS Code或Sublime Text等合适的编辑器打开 JSON。格式化和语法高亮将使其更容易被发现。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_14.png

图 5.14:高亮显示的聊天 ID

完成此步骤后,你现在已经创建了 TG 机器人,保存了凭据并识别了聊天 ID。通知现在有了确定的归处。当你打开数据与对话之间的界线时,自动化将感觉更有活力。

此之后,我们将把手动触发器连接到 HTTP 请求,并从 Yahoo 获取实时数据以丰富我们的工作流。

步骤 3:添加手动触发器并配置 HTTP 请求

在这一步中,我们引入了动态数据。你将添加一个手动触发器来启动工作流,然后配置 HTTP 请求节点以从 Yahoo 拉取信息。这些包括最活跃的股票、涨幅最高和跌幅最大的股票,它们非常适合分析。

添加手动触发器

manual trigger(手动触发器)允许你随时启动工作流,方便我们在进行过程中进行测试和完善。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_15.png

图 5.15:手动触发器节点

正如图 5.15所示,触发节点位于画布的最开始,确保每次执行都从这里开始。

添加第一个 HTTP 请求

接下来,我们将配置 HTTP 请求节点以从 Yahoo 拉取信息。这些包括最活跃的股票、涨幅最高和跌幅最大的股票,它们非常适合分析。

// ---- config
const LIMIT = 5;

这设置了排序后返回代码数量上限。在我们的案例中,列表限制为五个。

选择五个作为限制是为了快速获得结果。后续步骤将为每个代码调用外部服务,因此保持小数量可以避免不必要的请求。

现在,我们引入处理归一化和过滤的辅助函数:

function normNum(v) {
  if (typeof v === 'number' && isFinite(v)) return v;
  if (v == null) return 0;
  const n = parseFloat(String(v).replace('%','').trim());
  return isFinite(n) ? n : 0;
}

function cleanSymbol(s) {
  return (s || '').toString().trim().toUpperCase();
}

function isLikelyEquity(sym, quoteType) {
  if (!sym) return false;
  if (sym.startsWith('^')) return false;
  if (sym.includes('=')) return false;
  if (quoteType && quoteType !== 'EQUITY') return false;
  return true;
}
  • normNum(v)标准化百分比和数字

  • cleanSymbol(s)确保代码为大写并去除了空格

  • isLikelyEquity()移除非股票类工具(如指数和外汇对)

如果数学或财务逻辑看起来很晦涩,不要担心。你不需要完全掌握也能跟上工作坊的。查看代码作为参考就足够了。要点是理解这些辅助函数处于什么位置,而繁重的工作已经为你完成了。

下一部分处理从每个输入项中提取行:

function extractRowsFromItem(it) {
  const row = it?.json ?? it ?? {};
  const quotes = row.finance?.result?.[0]?.quotes;
  if (Array.isArray(quotes) && quotes.length) {
    return quotes.map(q => ({
      symbol: q.symbol,
      changePct: normNum(q.regularMarketChangePercent),
      quoteType: q.quoteType
    }));
  }
  return [{
    symbol: row.symbol || row.ticker,
    changePct: normNum(
      row.changePct ??
      row.regularMarketChangePercent ??
      row.changesPercentage ??
      row.percentchange
    ),
    quoteType: row.quoteType
  }];
}

这确保了我们能够一致地处理结构化的筛选器响应和单行输入。

准备完成后,我们可以将所有行收集到一个桶中:

const bucket = [];
for (const it of items) {
  const rows = extractRowsFromItem(it);
  for (const r of rows) bucket.push(r);
}

接下来,对代码去重并过滤掉无效内容:

const seen = new Set();
const flat = [];

for (const r of bucket) {
  const sym = cleanSymbol(r.symbol);
  if (!isLikelyEquity(sym, r.quoteType)) continue;
  if (seen.has(sym)) continue;
  seen.add(sym);
  flat.push({ symbol: sym, changePct: normNum(r.changePct) });
}

最后,我们按百分比变化的绝对值进行排序,并只取前五个:

flat.sort((a, b) => Math.abs(b.changePct) - Math.abs(a.changePct));
const top = flat.slice(0, LIMIT);
return top.map(x => ({ json: { symbol: x.symbol, changePct: x.changePct } }));

这产生了一个按波动幅度强弱排序的清洁股票列表。每行都以 JSON 格式输出,为下一步做准备。

当执行时,Code节点的输出将显示五个项目,每个项目包含一个代码符号及其百分比变化。

重新运行工作流

现在,从手动触发重新运行工作流。三个 HTTP 请求将全部执行,Merge节点将合并它们,Code节点将返回一个五个项目的列表。

打开Code节点查看输出。你应该看到类似这样的结构:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_22.png

图 5.22:Code 节点输出

图 5.22 展示了生成的五个代码列表,每个代码都有其百分比变化,证实了去重和排序逻辑是有效的。非常干净利落,不是吗?

[
  { "symbol": "PSTG", "changePct": 32.3365 },
  { "symbol": "ARX",  "changePct": -26.3571 },
  { "symbol": "SNOW", "changePct": 20.2655 },
  { "symbol": "BILL", "changePct": 18.184 },
  { "symbol": "TLX",  "changePct": -16.1157 }
]

JSON 输出是一个数组,每个项目包含一个symbol属性。该字段作为所有后续处理步骤的键,因为随着工作流的扩展,我们将为每个代码附加额外数据。

完成此步骤后,你现在拥有了跨三个类别的前五个波动去重排序列表。工作流不再仅仅是获取原始馈,它已经开始提供结构化的、排名的见解。

之后,我们将准备附加外部智能,为每个代码提供丰富的数据。

第 5 步:配置 Yahoo Finance

为了让决策基于实时市场背景,我们将从 Yahoo Finance 获取日内序列,并将每个代码的历史压缩为单条记录。我们将请求五天的五分钟 OHLCV (开盘价、最高价、最低价、收盘价、成交量) 线,然后计算快速信号——z-scores(标准化移动)和 ATR(平均真实范围,一种波动率指标),外加上简单的波动率标记来引导路由。

不要被OHLCV、ATR或z-score等缩写吓到。你不需要深奥数学即可跟随——代码已提供并可以直接复制。本章仅突出供参考的最有趣部分,因此关注流程而非公式。

HTTP Request 节点配置

  • Method: GET

  • URL: https://query1.yahoo.com/v8/finance/chart/{{ $json.symbol }}

  • Query Parameters: interval=5m, range=5d

此配置抓取了一个紧凑的日内窗口,易于评估趋势、冲击和参与度,而没有过大的负载。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_23.png

图 5.23:Yahoo 图表请求配置

在图 5.23中,请求显示的interval和range参数决定了每个代码返回的 OHLCV 数组。

在请求之后附加Code节点。它的作用是归一化输入并为每个代码返回一个紧凑的对象:最新价格、百分比变化、短期漂移 (zH)、最后一个 K 线冲击 (z1)、ATR 缩放移动、volume-spike标记以及组合的volatilityFlag:

// 解析器返回的最小形状
return [{
  json: { symbol, price, change, changePercent, z1, zH, moveVsATR, volSpike, volatilityFlag, timestamp }
}];

这种一进一出的约定确保了下游节点的可预测性——当符号数据稀疏时,不会出现列表爆炸或字段缺失。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_24.png

图 5.24:Yahoo 请求后的解析器

图 5.24 展示了布局:Code节点紧随 Yahoo 请求之后,将原始数组转换为单个包含丰富遥测数据的项。

连接到最后一个节点并从头开始执行,确认解析器为每个输入符号恰好输出一个项。当序列过短时,节点仍应返回一个安全的存根(stub),在避免错误的同时指示数据不足。

接下来,我们将通过 Google News 获取最近的标题,并为每个股票代码保留几个整洁的片段,以保持平衡且人类可读的上下文。

步骤 6:获取并解析新闻(Google News RSS,最近两天)

在这一中,你将为每个股票代码添加一个 Google News HTTP 请求,然后顺序运行两个Code节点:一个用于将每个 RSS 负载附加到其股票代码符号,另一个用于将 RSS XML 解析为紧凑的标题列表。

添加 Google News HTTP 请求

搜索HTTP Request,将其拖到画布上,并连接在你的 Yahoo 5d/5m指标节点之后(或者在分批的 Split in Batches 节点之后)。配置以下内容:

  • Method: GET

  • URL: https://news.google.com/rss/search?q={{$json.symbol}}%20stock%20when:2d&hl=en-US&gl=US&ceid=US:en

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_25.png

图 5.25:为每个股票代码配置 RSS 的 Google News HTTP 请求(最近两天)

在 Google News 之后连接两个 Code 节点

将两个Code节点拖到画布,并在Google News节点之后串联连接:

  • Attach Symbol 将每个 RSS 负载与正确的股票代码配对

  • Parse RSS 从 RSS XML 中提取标题(以及简短片段)到news[]数组

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_26.png

图 5.26:Google News HTTP 节点连接两个 Code 节点:Attach Symbol → Parse RSS

Code 节点 1:Attach Symbol

你可以从Code03_Attach_Symbol.js复制并粘贴完整脚本。核心思想是获取每个项返回的 RSS XML,并附加从你之前的去重节点获取的对应符号。两行代码最为重要,如下列表所示:

const SYMBOLS_NODE = 'Code - Dedupe and Sorting';  // <-- 必须与节点标签完全匹配
const symItems = $items(SYMBOLS_NODE, 0) || [];
  • SYMBOLS_NODE必须与输出{ symbol: ... }的节点的画布标签完全匹配(你的去重/排序步骤)

  • symItems获取该节点发送的所有项,为你提供与当前 RSS 项对齐的符号列表

文件的其余部分遍历输入项,将每个项与其符号配对,并输出{ symbol, data },其中data携带原始 RSS XML。

重要依赖

此新闻获取子流无法孤立运行。在未将其连接到产生股票代码符号的主工作流(来自Code – Dedupe and Sorting)之前,符号列表将是空的。这意味着没有内容可以请求新闻,也没有从 RSS 馈中解析任何内容。

如果你重命名了去重节点,请更新SYMBOLS_NODE为新名称。

Code 节点 2:Parse RSS

你可以从Code04_Parse_RSS.js复制并粘贴完整脚本。关键程序是parseRss(xml, symbol),它将原始 RSS XML 转换为紧凑的字符串列表,例如"标题 — 短描述":

function parseRss(xml, symbol) {
  const reItem = /<item\b[\s\S]*?<\/item>/gi;
  const blocks = Array.from(xml.matchAll(reItem)).map(m => m[0]);
  if (!blocks.length) return [];

  const reTitle = /<title>([\s\S]*?)<\/title>/i;
  const reDesc  = /<description>([\s\S]*?)<\/description>/i;
  const seen = new Set();
  const out = [];

  for (const block of blocks) {
    const t = stripHtml((block.match(reTitle)?.[1]) || '');
    const d = stripHtml((block.match(reDesc)?.[1]) || '');
    if (!t) continue;
    const key = t.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(d ? `${t} — ${d.slice(0, 240)}` : t);
    if (out.length >= 5) break;
  }

  return out;
}

以下是它的执行步骤:

  1. 在 XML 中定位每个<item> ... </item>块

  2. 提取每个项目的<title>(标题)和<description>(描述)

  3. 清理 HTML/CDATA 以确保保留可读文本

  4. 通过标题进行去重、截断片段,并限制在少数几个项目内以保持提示词(prompt)简洁

节点每个股票代码输出一个项目,格式为{ symbol, news: [ "...", "..." ] },供你的 LLM 步骤使用。

重要依赖项

此新闻抓取子流无法独立运行。在将其连接到生成股票代码符号的主工作流(来自Code – Dedupe and Sorting)之前,符号列表将为空。这意味着没有内容可以请求新闻,也没有从 RSS 馈中解析任何内容。

建立连接并验证运行

从Code – Dedupe and Sorting画出两条连接:

  • 连接到Yahoo Finance (5d/5m HTTP 请求)。

  • 连接到Google News (RSS HTTP 请求)。这是此子步骤中唯一的更改。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_27.png

图 5.27: 整体工作流:Code – Dedupe and Sorting 同时连接到 Yahoo Finance 和 Google News 分支

我们现在已经可以从头运行流水线,以确保目前没有错误。

重新运行工作流

从手动触发执行整个工作流。确认以下内容:

  • 每个节点都变为绿色

  • 两个分支都接收到了项目

  • 任何节点的执行面板中没有错误

在此阶段,市场指标和新闻馈送均已就绪,下一个任务是将它们整合。通过将每个股票代码的金融信号与其相关的标题对齐,我们为Mistral创建了统一的上下文。这为Step 7奠定了基础,在那里所有内容将整合为一个结构化的输入以进行深层分析。

Step 7:合并指标与新闻并准备 Mistral 提示词

现在 Yahoo Finance 指标和 Google News 标题已经在流转,我们准备好按股票代码将它们合并,并将一个整洁的、按符号分组的数据传递给Mistral,以生成简洁的买入/卖/持有建议。

设置合并块

放置一个Merge节点并配置为按字段合并:

  • Mode(模式): Combine

  • Combine by(合并依据): Matching fields

  • Fields to Match(待匹配字段): symbol

  • Inputs(输入):

    • 输入 1: 处理后的 Yahoo Finance 指标(你的 5d/5mCode输出)

    • 输入 2: 解析后的 Google News 标题(你的 Parse RSSCode输出)

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_28.png

图 5.28: Merge 节点配置,通过符号合并两个输入

运行节点。你现在应该看到每个股票代码对应一个项目,其中包含了金融指标和新闻数组。每个项目将携带{ symbol, ...financial fields..., news: [ ... ] }。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_29.png

图 5.29: 输出预览,显示每个合并项目包含符号、金融参数和新闻(标题/片段)

在此阶段,我们成功合并并精炼了金融指标和新闻馈送的输出,为每个符号创建了单一的丰富数据集。下一步是为Mistral准备这些信息。为此,我们将构建一个结构化的提示词,将金融指标与新闻上下文相结合,为模型提供清晰的视角以生成可操作的建议。

构建 Mistral 提示词

你可以从Code05_Build_Mistral_Prompt.js复制并粘贴完整的构建器,但让我们过一遍最重要的部分,这样你就确切知道它是工作的:

  1. 我们定义了小型辅助函数和限制。我们修剪长标题并只保留少数几个以控制 token;fmt安全地格式化数字:
const MAX_NEWS = 5;     // 只保留几个标题
const MAX_LINE = 300;   // 截断任何过长的行

function fmt(n, d = 2) { /* 返回 'n/a' 或固定小数 */ }

为什么?

这在保持提示词简短、可预测且廉价的同时,仍然保留了信息量。

  1. 然后我们遍历合并后的项目。我们为每个股票代码构建一个 Mistral 请求,因此每个符号都有自己的建议:
for (const itWrap of items) {
  const it = itWrap.json || {};
  // …
}

为什么?

按项目的调用使得路由、重试和下游发布(TG)变得简单。

  1. 接下来,我们组装news块。我们获取你的新闻数组(标题/片段)并将每个项渲染为项目符号;空列表将获得一个礼貌的占位符:
const newsLines = Array.isArray(it.news) ? it.news : [];

const newsBlock = newsLines
  .filter(Boolean)
  .slice(0, MAX_NEWS)
  .map(s => String(s).slice(0, MAX_LINE))
  .map(s => `- ${s}`)
  .join('\n') || '— no news —';

为什么?

Mistral 会看到一组紧凑、易扫描的、与该股票代码相关的最新标题。

  • 系统提示词 (System prompt):设置格式规则(适用于 TG、无 JSON、无代码块、转义特殊字符)

  • 模型与温度 (Model and temperature):小型、稳定的模型,输出方差低,以保持格式一致

预期的结果是 Mistral 在 choices[0].message.content 中返回一条单一 MarkdownV2 消息,可以直接发送到 TG。它看起来像这样:

  • *SYM*

  • 评分:*X/10*

  • 建议:*买入/持有/卖出*

  • 预测(1 周):*N.NN%*

  • 简短的理由和一行不确定性说明

现在 JSON 体已经就绪,下一步是将其传递到 HTTP 请求 (HTTP Request) 节点。这将我们将合并的财务和新闻数据传输到 Mistral API 进行分析。

第 8 步:将提示发送到 Mistral 并交付到 TG

在此阶段,我们已准备好调用 Mistral API,稍后将每个 MarkdownV2 响应转发到 TG:

  1. 我们首先开始创建 Mistral HTTP 请求。在你的 Code – Build Mistral Prompt 节点之后添加一个 HTTP Request (HTTP 请求) 节点。将 Method (方法) 设置为 POST,Body Content Type (主体内容类型) 设置为 JSON,并将 Body (主体) 设置为构建器的输出(例如 {{$json}})。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_30.png

图 5.30:为 Mistral 推理设置的 HTTP 请求节点

  1. 然后,配置 Mistral 身份验证。打开节点的 Authentication (身份验证) 下拉,选择 Mistral Cloud API(内置),并粘贴你的 Mistral API 访问密钥。将其保存为可重用的凭据(例如 Mistral – Workshop)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_31.png

图 5.31:将访问密钥存储为命名凭据的 Mistral Cloud API 凭据

  1. 点击 Execute Node (执行节点)。响应应包含每个项目的单条消息,可以通过 choices[0].message.content 访问,并且已格式化为 MarkdownV2(根据第 7 步)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_32.png

图 5.32:成功的 Mistral 响应

  1. 从 Mistral 安全返回响应后,我们现在将焦点转移到提取实际的建议文本。关键内容嵌套在 JSON 回复中,位于 {{$json["choices"][0]["message"]["content"]}} 路径下。该值包含了我们将向下传递的洞察,它是模型输出与我们最终交付通道之间的桥梁。

  2. 我们通过添加 TG → Send Message (发送消息) 节点并连接在 Mistral 节点之后来连接 TG。在 Text (文本) 中,直接使用助手内容,如下例所示:

  • Text (文本)(表达式):{{$json["choices"][0]["message"]["content"]}}

  • Parse Mode (解析模式):MarkdownV2

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_33.png

图 5.33:带有 Chat ID 和凭据的 TG 发消息

  1. 如果尚未配置 TG 凭据,你必须在 n8n 中进行设置。请使用从 @BotFather 接收到的访问令牌。将此令牌作为 TG 凭据输入工作流。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_34.png

图 5.34:使用访问令牌设置的 TG 账户凭据

到此时,我们即将成功完成我们的解决方案。随着 TG 的完全连接,已经准备好将流水线作为一个无缝的工作流运行,确保每个节点都能正确处理并无错误地将洞察交付到聊天中。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/1.png 快速提示:需要查看此图像的高分辨率版本吗?在下一代 Packt Reader 中打开此书,或在 PDF/ePub 副本中查看。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/2.png 购买此书时免费赠送下一代 Packt Reader。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称找到此书。双检显示的版本以确保你获得的是正确的版本。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/Unlock2.png

第 9 步:运行整个工作流

从手动触发开始运行整个工作流。确保所有节点都成功执行且没有错误,并且每个项目都流到最后,交付 TG 消息。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_35.png

图 5.35:最终成功的工作流

在手机上打开你的 TG 聊天——你应该能看到交付了五条消息,每条消息都包含符号、评分、建议、周预测和理由的财务洞察,就像屏幕上显示的一样。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_05_36.png

图 5.36:显示目标聊天中收到的 TG 消息的手机截图

通过完成这一步,我们实现了一个端到端的工作流,从市场数据和新闻收集,通过 Mistral 分析,到清晰交付。

总结

本工作坊逐步让智能的理论变为现实。我们从设置依赖项和验证对 Mistral API 的连接开始。有了这些基础后,我们构建了第一个工具——一个简单的计算器——然后将其封装为智能。从那里,通过添加网络搜索和组装智能,我们扩展了能力。旅程继续到针对角色的智能,并在一个协作解决任务的多智能系统中达到。最后,交互演示让我们测试这些系统,观察它们的推理和适应。实践课程很明确:智能已经是可用、可适应且有价值的。

在深入探索智能之后,我们准备好迎接下一次飞跃:检索增强生成 (RAG)。如果说智能给了我们适应性,RAG 则给了我们增强版的记忆——在海量基础中进行推理的能力。在接下来的章节中,你将看到检索和生成如何融合为一个强大的工作流。

课外活动

在建立了嵌入工作的良好基础后,你现在准备好探索更高级、更有创意的应用。让这些练习为你的项目激发灵感:

  • 构建自定义 n8n 流水线:从零开始并在 n8n 内部设计你自己的工作流。将数据馈、AI 分析和交付通道相结合,根据独特目标定制快速原型。

  • 连接 n8n 与 Discord:通过将 n8n 与 Webhooks 或机器人集成来扩展你的工作流。这让洞察自动流向专用频道,方便与社区实时协作。

  • 在 n8n 中嵌入图表:通过附加蜡烛图或技术研究增强你的工作流。这创建了丰富的更新,帮助快速理解洞察而非阅读文本。

  • 使用 n8n 内置节点替换代码节点:使用 n8n 的原生节点(如 Set、IF、Merge 或 Math)重构工作流部分。这减少了代码复杂性,并突出了 n8n 的无代码潜力。

  • 在代码中重新实现 n8n 流水线:将你在 n8n 构建的自动化工作流在 Python 或 Node.js 中完全复制。这项练习加强了编码技能,并强化了每一步后的自动化逻辑。

  • 使用 Make (Integromat) 进行实验:设计一个多步骤场景,从 API 获取数据,根据条件分支并做出反应。注意添加推理 (LLM) 如何将其提升纯自动化。

  • 混合智能挑战:选择一个任务(例如总结会议记录,然后自动更新 Trello)。使用 Zapier 等无代码工具构建 LLM 智能框架。反思每个部分的闪光之处。

加入我们的 Discord 和 Reddit 空间

你不是唯一在应对碎片化工具、不断更新和不明最佳实践的人。加入不断成长的人士社区,交换文档化的洞察。

| 通过更新、讨论和作者的幕后洞察保持联系。加入我们的 Discord packtpub.com/z8ivB 或扫描下方二维码:(Images/Discord_Code.jpg) | 在 Reddit 上关注我们,分享想法并讨论真实的 GenAI 挑战。访问 packtpub.com/0rExL 或扫描下方二维码:(Images/Reddit_Code.png) |

6

拆解 RAG 工作流

发现的旅程不在于寻找景观,而在于拥有新的视角。

— 马塞尔·普鲁

语言模型通常在大量的可用数据上训练。虽然这让它们对世界有了广泛的理解,但也意味着它们的知识在训练结束时就冻结了。但如果你有自己的知识——内部文档、隐私数据集或领域特定事实——并且希望模型根据这些内容生成答案呢?如果你需要每个用户查询都基于事实、根据你的来源进行检查,并由最新信息塑造呢?这正是检索增强生成 (RAG) 的用用场。

本章节是你引导 RAG 的指南,就像在进行海洋航行一样,每个停站点都揭示了 RAG 过程的关键部分:索引、检索和生成。通过实践工作坊,我们将遍历 RAG 旅程的每一个停站点,从基础实现到多查询路由和精细索引等高级技术。到结束时,你将拥有从容导航 RAG 的技能,构建无缝处理复杂问题的稳健系统。

简而言之,本章节将涵盖以下主题:

  • 问题声明

  • RAG 架构

  • 工作坊 1:引导 RAG 实现

  • 工作坊 2:多查询方法

  • 工作坊 3:缩放原子子查询

  • 工作坊 4:生成方法

技术要求

为了有效实现 RAG,某些工具和 API 密钥是必不可少的。以下是所需的技术要求清单:

  • Google Colab:方便的云端编码和实验

  • Pinecone API key:访问索引的向量数据库

  • Mistral AI API key:用于生成的语言模型集成

  • LangSmith API key:高级语言处理和分析

问题声明

在如今数据丰富的世界中,在巨大的数据库中寻找精确且上下文相关的信息可能是令人生畏的。RAG 通过结合数据检索与生成式 AI 提供了一种解决方案,以提供全面的响应。本章节解决了这一问题,通过结合数据检索与生成式 AI 提供全面的响应。

RAG 架构

在此阶段,我们已准备好像智能体一样,在海洋中,每个停站点都揭示了 RAG 过程的关键部分:索引、检索和生成。通过实践工作坊,我们将遍历 RAG 旅程的每一个停站点,从基础实现到多查询路由和精细索引等高级技术。到结束时,你将拥有从容导航 RAG 的技能,构建无缝处理复杂问题的稳健系统。

简而言之,本章节将涵盖以下主题:

  • 问题声明

  • RAG 架构

  • 工作坊 1:引导 RAG 实现

  • 工作坊 2:多查询方法

  • 工作坊 3:缩放原子子查询

  • 工作坊 4:生成方法

技术要求

为了有效实现 RAG,某些工具和 API 密钥是必不可少的。以下是所需的技术要求清单:

  • Google Colab:方便的云端编码和实验

  • Pinecone API key:访问索引的向量数据库

  • Mistral AI API key:用于生成的语言模型集成

  • LangSmith API key:高级语言处理和分析

(注:由于原文中部分关于 Mistral 的描述在后文中出现了重复或逻辑错位,翻译已根据上下文语义进行了微调以确保中文通顺。)

from langchain_community.document_loaders import WebBaseLoader

# Load from a web source
loader = WebBaseLoader(
    "https://www.gutenberg.org/cache/epub/2701/pg2701.txt")
books = loader.load()

在这段代码中,WebBaseLoader 从提供的 URL 加载纯文本文档,捕捉内容以开始索引。

文本切片 (Slicing the text)

为了为索引准备数据,通常将其拆分成更小的段落或“块”(chunks)。文本切片允许我们将文档分为若干部分,通过隔离相关的信息部分来提高检索效率。

  • 块大小 (Chunk size):这定义了每个段的最大长度。在 RAG 中,较小的块大小会提高检索精确度,但可能会增加块的数量。

  • 块重叠 (Chunk overlap):块之间的重叠确保了文本在分割时不会丢失上下文。通过重叠,每个块都包含其相邻块的部分,从而保持了信息的连续性。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/learn-mistral/img/B24489_06_03.png

图 6.3:带重叠的文本切片

该图演示了带重叠的文本切片,将较大的文本分为较小的块,每个块都包含来自相邻块的重叠部分。这种重叠确保了上下文的连续性,从而在下游过程中实现更准确的检索和生成。以下是一个文本切片的简单示例:

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=300,
    chunk_overlap=50)

splits = text_splitter.split_documents(books)
# assume Books 是一个长文本集合

在这个示例中,chunk_size 设置为 300,意味着每个块最多包含 300 个 token,而 chunk_overlap 设置为 50,意味着每个块与其相邻块重叠 50 个 token。这种重叠使段落保持上下文保持连接,有助于提高检索准确性。

嵌入 (Embeddings)

一旦处理完文本,就会生成嵌入并存储在向量数据库中用于相似性搜索。嵌入将文本转换为捕捉语义含义的数值向量,这一概念在第 3 章中进行了详细探讨。

from langchain_mistralai import MistralAIEmbeddings

embd = MistralAIEmbeddings()
query_result = embd.embed_query(question)
document_result = embd.embed_query(document)

此示例发生了以下操作:

  • MistralAIEmbeddings 为查询和文档生成嵌入

  • embed_query 将文本转换为向量以供存储或比较

总结来说,索引阶段从文件或网络等源加载文档开始,然后进行分词并将文本切片为可管理的的块。每个块随后被转换为向量嵌入(或图密码、SQL 语句),捕捉语义含义,并保存到 VectorDB 等索引存储中,以确保 RAG 应用的高效检索。

检索 (Retrieval)

当接收到用户查询时,检索就会激活。查询以原始形式或在拆分为更简单的子查询后,被发送到检索器(retriever)。检索器的作用是咨询索引阶段创建的索引,扫描它们以寻找最相关的信息。这可能涉及跨不同类型的存储搜索——例如数据湖、SQL 或 NoSQL 数据库,甚至是云存储库——取决于所需数据所在的位置。检索的目标是识别并加载与问题最相关的信息,通常以文本段、元数据或直接相关查询上下文的总结性解的形式存在。

为了实现最佳相关性,检索可以使用先进搜索方法,包括向量数据库中的相似性搜索、图数据库中的图遍历或 SQL 数据库中的结构化查询。此外,LLM 可以通过转换或重述用户查询来使其更精确,甚至将复杂问题分解为更小、更有针对性的查询来提供协助。这些技术可以进一步提高查询与检索信息之间的匹配度。因此,检索是为了弥合索引知识库与用户问题之间的差距,确保只有最相关的数据被送到生成器。

在接下来的检索示例中,我们将使用 Pinecone 作为向量数据库来检索相关文档。Pinecone 高效的基于相似性的向量搜索允许我们快速定位与上下文最相关的片段,使其成为 RAG 检索的强大选择。

准备检索器 (Preparing the retriever)

检索的第一步是创建一个 retriever 对象,它将在向量存储 Pinecone 中搜索最相关的文档嵌入。这段代码段设置了检索器:

from langchain_mistralai import MistralAIEmbeddings
from langchain_community.vectorstores import Pinecone

### 使用 Mistral 嵌入初始化向量存储
### 以及已索引的文档
vectorstore = Pinecone.from_documents(
    index_name='book-passages',
    documents=splits,
    embedding=MistralAIEmbeddings()
)

# 使用指定的搜索参数创建检索器
# (k=1 表示获取顶部结果)
retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
  • Pinecone.from_documents(...):这使用 book-passages 索引初始化向量存储,其中文档代表预处理过的拆分文本块

  • MistralAIEmbeddings():此函数为每个文档生成嵌入,捕捉语义含义以实现准确检索

  • retriever = vectorstore.as_retriever(...):创建一个 k=1 的检索器,它将结果限制为每个查询中最相关的一个文档。

在本章中,我们使用封装嵌入和检索过程的 LangChain 封装。这种方法通过像积木一样组合组件简化了集成,允许我们无缝使用嵌入和 Pinecone,无需直接交互 Pinecone SDK。

从向量存储检索文档

准备好检索器后,我们可以使用特定问题查询它以检索相关文档:

# 根据用户查询检索相关文档
docs = retriever.get_relevant_documents(
    "白比号中的船叫什么名字?")

retriever.get_relevant_documents(...) 执行搜索查询。在这里,它定位了 What is the name of the ship in Moby Dick? 的最匹配文档。

此次检索的结果将是与问题相关的片段集合(在我们的案例中,由于 k=1,所以只是一个项目)。这还不是答案;它只是可能包含答案的文本块。

在检索阶段,我们使用 Pinecone 初始化了一个向量检索器,它能够对预索引的文档嵌入进行高效的基于相似性的搜索。该检索器设置有助于为每个用户查询定位最相关的文档片段。

现在已经涵盖了检索部分,我们可以使用获取的信息作为上下文,来准备智能的摘要化的答案——生成阶段。

生成

生成是答案成型的阶段,将检索到的数据转换为连贯的、可读的响应。在 LLM 的驱动下,生成器会将检索到的信息结合用户的原始问题、任何现有上下文(如之前的聊天历史)以及知识库中的元数据进行合成。LLM 处理整个输入集,以构建出读起来自然、准确回答查询并保持上下文连续性的响应。

生成阶段可能会多次调用 LLM。例如,LLM 可能会从不同的角度回答问题,或者在将这些响应合成一个连贯答案之前探索额外的角度。这种多步方法对于需要细致回答的复杂查询特别有用,因为它允许系统在得出最终答案之前考虑问题的各个方面。此外,LLM 可能会采用摘要技术来精炼输出,过滤掉冗余信息并确保清晰度。

在 RAG 工作流中,LLM 的主要作用是增强生成,但它也可以通过重构问题或总结检索到的数据来支持其他阶段。通过在多个阶段利用 LLM,RAG 系统创建了既有见解又易于理解的答案。因此,生成是将原始数据转换为可操作洞的最后一步,确保用户收到清晰、全面的响应。

posted @ 2026-09-25 20:42  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报