GSM8K:GSM8K是一个包含小学数学问题的数据集,用于测试模型的数学推理和逻辑分析能力。具体任务包括算术运算、简单方程求解、数字推理等。GSM8K中的问题虽然看似简单,但模型需要理解问题语义并进行正确的数学运算,体现了逻辑推理和语言理解的双重挑战。
ARC Challenge:ARC Challenge评测模型在科学推理任务中的表现,尤其是常识性和科学性问题的解答,典型应用场景包括科学考试题解答和百科问答系统的开发。
GPQA:用于评测模型在零样本条件下对开放性问题的回答能力,通常应用于客服聊天机器人和知识问答系统中,帮助模型在缺乏特定领域数据的情况下给出合理的回答。
InfiniteBench/En.MC:评测模型在处理长文本阅读理解方面的能力,尤其是对科学文献的理解,适用于学术文献自动摘要、长篇报道分析等应用场景。
医疗榜:基于MedBench评测集,评估大语言模型在医学知识问答、安全伦理理解等方面的表现。由上海人工智能实验室提供。
向量化模块:用来将文档片段向量化。
文档加载和切分模块:用来加载文档并切分成文档片段。
数据库:存放文档片段及其对应的向量表示。
检索模块:根据 Query(问题)检索相关的文档片段。
索引:将文档库分割成较短的片段,并通过编码器构建向量索引。
检索:根据问题和片段的相似度检索相关文档片段。
def read_file_content(cls, file_path: str):
# 根据文件扩展名选择读取方法
if file_path.endswith('.pdf'):
return cls.read_pdf(file_path)
elif file_path.endswith('.md'):
return cls.read_markdown(file_path)
elif file_path.endswith('.txt'):
return cls.read_text(file_path)
else:
raise ValueError("Unsupported file type")
n粗切分),并保证片段之间有一些重叠内容,以提高检索的准确性。
def get_chunk(cls, text: str, max_token_len: int = 600, cover_content: int = 150):
chunk_text = []
curr_len = 0
curr_chunk = ''
token_len = max_token_len - cover_content
lines = text.splitlines() # 假设以换行符分割文本为行
for line in lines:
# 保留空格,只移除行首行尾空格
line = line.strip()
line_len = len(enc.encode(line))
if line_len > max_token_len:
# 如果单行长度就超过限制,则将其分割成多个块
# 先保存当前块(如果有内容)
if curr_chunk:
chunk_text.append(curr_chunk)
curr_chunk = ''
curr_len = 0
# 将长行按token长度分割
line_tokens = enc.encode(line)
num_chunks = (len(line_tokens) + token_len - 1) // token_len
for i in range(num_chunks):
start_token = i * token_len
end_token = min(start_token + token_len, len(line_tokens))
# 解码token片段回文本
chunk_tokens = line_tokens[start_token:end_token]
chunk_part = enc.decode(chunk_tokens)
# 添加覆盖内容(除了第一个块)
if i > 0 and chunk_text:
prev_chunk = chunk_text[-1]
cover_part = prev_chunk[-cover_content:] if len(prev_chunk) > cover_content else prev_chunk
chunk_part = cover_part + chunk_part
chunk_text.append(chunk_part)
# 重置当前块状态
curr_chunk = ''
curr_len = 0
elif curr_len + line_len + 1 <= token_len: # +1 for newline
# 当前行可以加入当前块
if curr_chunk:
curr_chunk += 'n'
curr_len += 1
curr_chunk += line
curr_len += line_len
else:
# 当前行无法加入当前块,开始新块
if curr_chunk:
chunk_text.append(curr_chunk)
# 开始新块,添加覆盖内容
if chunk_text:
prev_chunk = chunk_text[-1]
cover_part = prev_chunk[-cover_content:] if len(prev_chunk) > cover_content else prev_chunk
curr_chunk = cover_part + 'n' + line
curr_len = len(enc.encode(cover_part)) + 1 + line_len
else:
curr_chunk = line
curr_len = line_len
# 添加最后一个块(如果有内容)
if curr_chunk:
chunk_text.append(curr_chunk)
return chunk_text
BaseEmbeddings 基类,这样我们在使用其他模型时,只需要继承这个基类,然后在此基础上进行修改即可,方便代码扩展。
class BaseEmbeddings:
"""
Base class for embeddings
"""
def __init__(self, path: str, is_api: bool) -> None:
"""
初始化嵌入基类
Args:
path (str): 模型或数据的路径
is_api (bool): 是否使用API方式。True表示使用在线API服务,False表示使用本地模型
"""
self.path = path
self.is_api = is_api
def get_embedding(self, text: str, model: str) -> List[float]:
"""
获取文本的嵌入向量表示
Args:
text (str): 输入文本
model (str): 使用的模型名称
Returns:
List[float]: 文本的嵌入向量
Raises:
NotImplementedError: 该方法需要在子类中实现
"""
raise NotImplementedError
@classmethod
def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float:
"""
计算两个向量之间的余弦相似度
Args:
vector1 (List[float]): 第一个向量
vector2 (List[float]): 第二个向量
Returns:
float: 两个向量的余弦相似度,范围在[-1,1]之间
"""
# 将输入列表转换为numpy数组,并指定数据类型为float32
v1 = np.array(vector1, dtype=np.float32)
v2 = np.array(vector2, dtype=np.float32)
# 检查向量中是否包含无穷大或NaN值
if not np.all(np.isfinite(v1)) or not np.all(np.isfinite(v2)):
return 0.0
# 计算向量的点积
dot_product = np.dot(v1, v2)
# 计算向量的范数(长度)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
# 计算分母(两个向量范数的乘积)
magnitude = norm_v1 * norm_v2
# 处理分母为0的特殊情况
if magnitude == 0:
return 0.0
# 返回余弦相似度
return dot_product / magnitude
BaseEmbeddings基类有两个主要方法:get_embedding和cosine_similarity。get_embedding用于获取文本的向量表示,cosine_similarity用于计算两个向量之间的余弦相似度。在初始化类时设置了模型的路径和是否是API模型,例如使用OpenAI的Embedding API需要设置self.is_api=True。
BaseEmbeddings类只需要实现get_embedding方法,cosine_similarity方法会被继承下来。这就是编写基类的好处。
class OpenAIEmbedding(BaseEmbeddings):
"""
class for OpenAI embeddings
"""
def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api)
if self.is_api:
self.client = OpenAI()
# 从环境变量中获取 硅基流动 密钥
self.client.api_key = os.getenv("OPENAI_API_KEY")
# 从环境变量中获取 硅基流动 的基础URL
self.client.base_url = os.getenv("OPENAI_BASE_URL")
def get_embedding(self, text: str, model: str = "BAAI/bge-m3") -> List[float]:
"""
此处默认使用硅基流动的免费嵌入模型 BAAI/bge-m3
"""
if self.is_api:
text = text.replace("n", " ")
return self.client.embeddings.create(input=[text], model=model).data[0].embedding
else:
raise NotImplementedError
注:此处我们默认使用国内用户可访问的硅基流动大模型API服务平台。
persist:数据库持久化保存。
load_vector:从本地加载数据库。
get_vector:获取文档的向量表示。
query:根据问题检索相关文档片段。
class VectorStore:
def __init__(self, document: List[str] = ['']) -> None:
self.document = document
def get_vector(self, EmbeddingModel: BaseEmbeddings) -> List[List[float]]:
# 获得文档的向量表示
pass
def persist(self, path: str = 'storage'):
# 数据库持久化保存
pass
def load_vector(self, path: str = 'storage'):
# 从本地加载数据库
pass
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
# 根据问题检索相关文档片段
pass
query 方法用于将用户提出的问题向量化,然后在数据库中检索相关文档片段并返回结果。
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
query_vector = EmbeddingModel.get_embedding(query)
result = np.array([self.get_similarity(query_vector, vector) for vector in self.vectors])
return np.array(self.document)[result.argsort()[-k:][::-1]].tolist()
class BaseModel:
def __init__(self, path: str = '') -> None:
self.path = path
def chat(self, prompt: str, history: List[dict], content: str) -> str:
pass
def load_model(self):
pass
BaseModel 包含两个方法:chat和load_model。对于本地化运行的开源模型需要实现load_model,而API模型则不需要。在此处我们还是使用国内用户可访问的硅基流动大模型API服务平台,使用API服务的好处就是用户不需要本地的计算资源,可以大大降低学习者的学习门槛。
from openai import OpenAI
class OpenAIChat(BaseModel):
def __init__(self, model: str = "Qwen/Qwen2.5-32B-Instruct") -> None:
self.model = model
def chat(self, prompt: str, history: List[dict], content: str) -> str:
client = OpenAI()
client.api_key = os.getenv("OPENAI_API_KEY")
client.base_url = os.getenv("OPENAI_BASE_URL")
history.append({'role': 'user', 'content': RAG_PROMPT_TEMPLATE.format(question=prompt, context=content)})
response = client.chat.completions.create(
model=self.model,
messages=history,
max_tokens=2048,
temperature=0.1
)
return response.choices[0].message.content
RAG_PROMPT_TEMPLATE="""
使用以上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。
问题: {question}
可参考的上下文:
···
{context}
···
如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。
有用的回答:
"""
from VectorBase import VectorStore
from utils import ReadFiles
from LLM import OpenAIChat
from Embeddings import OpenAIEmbedding
# 没有保存数据库
docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150) # 获得data目录下的所有文件内容并分割
vector = VectorStore(docs)
embedding = OpenAIEmbedding() # 创建EmbeddingModel
vector.get_vector(EmbeddingModel=embedding)
vector.persist(path='storage') # 将向量和文档内容保存到storage目录下,下次再用就可以直接加载本地的数据库
# vector.load_vector('./storage') # 加载本地的数据库
question = 'RAG的原理是什么?'
content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = OpenAIChat(model='Qwen/Qwen2.5-32B-Instruct')
print(chat.chat(question, [], content))
from VectorBase import VectorStore
from utils import ReadFiles
from LLM import OpenAIChat
from Embeddings import OpenAIEmbedding
# 保存数据库之后
vector = VectorStore()
vector.load_vector('./storage') # 加载本地的数据库
question = 'RAG的原理是什么?'
embedding = ZhipuEmbedding() # 创建EmbeddingModel
content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = OpenAIChat(model='Qwen/Qwen2.5-32B-Instruct')
print(chat.chat(question, [], content))
注:7.2 章节的所有代码均可在 Happy-LLM Chapter7 RAG 中找到。
理解目标(Goal Understanding): 接收一个相对复杂或高层次的目标(例如,“帮我规划一个周末去北京的旅游行程并预订机票酒店”)。
自主规划(Planning): 将大目标分解成一系列可执行的小步骤(例如,“搜索北京景点”、“查询天气”、“比较机票价格”、“查找合适的酒店”、“调用预订API”等)。
记忆(Memory): 拥有短期记忆(记住当前任务的上下文)和长期记忆(从过去的交互或外部知识库中学习和检索信息)。
工具使用(Tool Use): 调用外部API、插件或代码执行环境来获取信息(如搜索引擎、数据库)、执行操作(如发送邮件、预订服务)或进行计算。
特点: 专注于完成特定领域的、定义明确的任务,例如客户服务、代码生成、数据分析等。
工作方式: 通常有预设的流程和可调用的特定工具集。LLM主要负责理解用户意图、填充任务槽位、生成回应或调用合适- 的工具。
特点: 强调自主分解复杂任务、制定多步计划,并根据环境反馈进行调整的能力。它们通常需要更强的推理能力。
工作方式: 常采用特定的思维框架,如ReAct (Reason+Act),让模型先进行“思考”(Reasoning)分析当前情况和所需行动,然后执行“行动”(Action)调用工具,再根据工具返回结果进行下一轮思考。Chain-of-Thought (CoT) 等提示工程技术也是其推理的基础。
特点: 由多个具有不同角色或能力的Agent协同工作,共同完成一个更宏大的目标。
工作方式: Agent之间可以进行通信、协作、辩论甚至竞争。例如,一个Agent负责规划,一个负责执行,一个负责审查。
特点: 这类Agent不仅执行任务,还能在与环境的交互中主动学习新知识、新技能或优化自身策略,类似于强化学习中的Agent概念。
工作方式: 可能包含更复杂的记忆和反思机制,能够根据成功或失败的经验调整未来的规划和行动。
openai 库和其 tool_calls 功能,动手构造一个 Tiny-Agent,这个 Agent 是一个简单的任务导向型 Agent,它能够根据用户的输入,回答一些简单的问题。
openai 库,并配置其指向一个兼容 OpenAI API 的服务终端,例如 SiliconFlow。同时,指定要使用的模型,如 Qwen/Qwen2.5-32B-Instruct。
from openai import OpenAI
# 初始化 OpenAI 客户端
client = OpenAI(
api_key="YOUR_API_KEY", # 替换为你的 API Key
base_url="https://api.siliconflow.cn/v1", # 使用 SiliconFlow 的 API 地址
)
# 指定模型名称
model_name = "Qwen/Qwen2.5-32B-Instruct"
注意: 你需要将YOUR_API_KEY替换为你从 SiliconFlow 或其他服务商获取的有效 API Key。
src/tools.py 文件中定义 Agent 可以使用的工具函数。每个函数都需要有清晰的文档字符串(docstring),描述其功能和参数,因为这将用于自动生成工具的 JSON Schema。
# src/tools.py
from datetime import datetime
# 获取当前日期和时间
def get_current_datetime() -> str:
"""
获取当前日期和时间。
:return: 当前日期和时间的字符串表示。
"""
current_datetime = datetime.now()
formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
return formatted_datetime
def count_letter_in_string(a: str, b: str):
"""
统计字符串中某个字母的出现次数。
:param a: 要搜索的字符串。
:param b: 要统计的字母。
:return: 字母在字符串中出现的次数。
"""
return str(a.count(b))
def search_wikipedia(query: str) -> str:
"""
在维基百科中搜索指定查询的前三个页面摘要。
:param query: 要搜索的查询字符串。
:return: 包含前三个页面摘要的字符串。
"""
page_titles = wikipedia.search(query)
summaries = []
for page_title in page_titles[: 3]: # 取前三个页面标题
try:
# 使用 wikipedia 模块的 page 函数,获取指定标题的维基百科页面对象。
wiki_page = wikipedia.page(title=page_title, auto_suggest=False)
# 获取页面摘要
summaries.append(f"页面: {page_title}n摘要: {wiki_page.summary}")
except (
wikipedia.exceptions.PageError,
wikipedia.exceptions.DisambiguationError,
):
pass
if not summaries:
return "维基百科没有搜索到合适的结果"
return "nn".join(summaries)
# ... (可能还有其他工具函数)
src/utils.py 中的 function_to_json 辅助函数完成。
# src/utils.py (部分)
import inspect
def function_to_json(func) -> dict:
# ... (函数实现细节)
# 返回符合 OpenAI tool schema 的字典
return {
"type": "function",
"function": {
"name": func.__name__,
"description": inspect.getdoc(func),
"parameters": {
"type": "object",
"properties": parameters,
"required": required,
},
},
}
src/core.py 文件中定义 Agent 类。这个类负责管理对话历史、调用 OpenAI API、处理工具调用请求以及执行工具函数。
# src/core.py (部分)
from openai import OpenAI
import json
from typing import List, Dict, Any
from utils import function_to_json
# 导入定义好的工具函数
from tools import get_current_datetime, add, compare, count_letter_in_string
SYSTEM_PROMPT = """
你是一个叫不要葱姜蒜的人工智能助手。你的输出应该与用户的语言保持一致。
当用户的问题需要调用工具时,你可以从提供的工具列表中调用适当的工具函数。
"""
class Agent:
def __init__(self, client: OpenAI, model: str = "Qwen/Qwen2.5-32B-Instruct", tools: List=[], verbose : bool = True):
self.client = client
self.tools = tools
self.model = model
self.messages = [
{"role": "system", "content": SYSREM_PROMPT},
]
self.verbose = verbose
def get_tool_schema(self) -> List[Dict[str, Any]]:
# 获取所有工具的 JSON 模式
return [function_to_json(tool) for tool in self.tools]
def handle_tool_call(self, tool_call):
# 处理工具调用
function_name = tool_call.function.name
function_args = tool_call.function.arguments
function_id = tool_call.id
function_call_content = eval(f"{function_name}(**{function_args})")
return {
"role": "tool",
"content": function_call_content,
"tool_call_id": function_id,
}
def get_completion(self, prompt) -> str:
self.messages.append({"role": "user", "content": prompt})
# 获取模型的完成响应
response = self.client.chat.completions.create(
model=self.model,
messages=self.messages,
tools=self.get_tool_schema(),
stream=False,
)
# 检查模型是否调用了工具
if response.choices[0].message.tool_calls:
self.messages.append({"role": "assistant", "content": response.choices[0].message.content})
# 处理工具调用
tool_list = []
for tool_call in response.choices[0].message.tool_calls:
# 处理工具调用并将结果添加到消息列表中
self.messages.append(self.handle_tool_call(tool_call))
tool_list.append([tool_call.function.name, tool_call.function.arguments])
if self.verbose:
print("调用工具:", response.choices[0].message.content, tool_list)
# 再次获取模型的完成响应,这次包含工具调用的结果
response = self.client.chat.completions.create(
model=self.model,
messages=self.messages,
tools=self.get_tool_schema(),
stream=False,
)
# 将模型的完成响应添加到消息列表中
self.messages.append({"role": "assistant", "content": response.choices[0].message.content})
return response.choices[0].message.content
接收用户输入。
调用大模型(如 Qwen),并告知其可用的工具及其 Schema。
如果模型决定调用工具,Agent 会解析请求,执行相应的 Python 函数。
Agent 将工具的执行结果返回给模型。
模型根据工具结果生成最终回复。
demo.py 的 if __name__ == "__main__": 部分提供了一个简单的命令行交互示例。
# demo.py (部分)
if __name__ == "__main__":
client = OpenAI(
api_key="YOUR_API_KEY", # 替换为你的 API Key
base_url="https://api.siliconflow.cn/v1",
)
# 创建 Agent 实例,传入 client、模型名称和工具函数列表
agent = Agent(
client=client,
model="Qwen/Qwen2.5-32B-Instruct",
tools=[get_current_datetime, add, compare, count_letter_in_string],
verbose=True # 设置为 True 可以看到工具调用信息
)
# 开始交互式对话循环
while True:
# 使用彩色输出区分用户输入和AI回答
prompt = input("