老爹疯狂厨房
125.32M · 2026-03-31
书接上文:AI Agent案例实践:三种智能体开发模式详解之二(基于LangChain框架)
前文中,我们使用LangChain框架实现了我们的智能体。下面让我们使用Qwen-Agent实现一版ReAct,体验下流程,具体的业务场景中需要根据需求,调研后决定具体使用哪种方式。
Qwen-Agent 开发框架支持 ReAct 范式
Qwen-Agent 提供了 ReActChat 类来支持 ReAct 推理范式。这允许智能体像人类一样进行思考(Reasoning)、行动(Taking Action)(例如调用工具)和观察(Observing) 结果,并循环此过程直至任务完成 。
关于迭代次数
在 Qwen-Agent 的上下文中,我们通常说的“迭代”指的是 ReAct 循环中模型调用工具并进行推理的轮次。
实用建议
接下来我们直接使用QwenAgent框架实现ReAct智能体模式。
具体开发实现如下(agent_service_qwen.py):
mport sys
import json
from qwen_agent.agents import Assistant
from qwen_agent.tools import BaseTool
from api_service import QueryService, SemanticServce, AnalysisService
# 初始化服务
queryService = QueryService()
semanticService = SemanticServce()
analysisService = AnalysisService()
# 自定义工具类
class MatchMetadataTool(BaseTool):
name = 'match_metadata'
description = '根据输入文本语义匹配表结构,每次返回一个最相关的表结构。对于需要多表查询的问题,需要多次调用此工具。'
def call(self, params, **kwargs) -> str:
table = semanticService.hybrid_search(params, 1)
return f"{[t['table_info'] for t in table]}"
class ExecuteSQLTool(BaseTool):
name = 'execute_sql'
description = '执行SQL查询并返回结果。输入应为标准SQL语句。注意:可能需要执行多个SQL查询来获取不同表中的数据。'
def call(self, params, **kwargs) -> str:
if isinstance(params, str):
jsonObj = json.loads(params)
if 'sql'in jsonObj:
params = jsonObj['sql']
if 'query'in jsonObj:
params = jsonObj['query']
if isinstance(params, dict):
if 'sql'in params:
params = params['sql']
if 'query'in params:
params = params['query']
if params.endswith(';'):
params = params[:-1]
return json.dumps(queryService.query_with_column(params))
# 创建Agent实例
agent = Assistant(
name='ai_agent_assistant',
llm={
'model': 'qwen3:32b',
'model_server': 'http://localhost:11434/v1',
},
system_message="""
你是一个数据分析助手,负责帮助用户查询数据库信息。
请特别注意:用户的问题可能需要从多个表中查询数据。
1. 首先确定需要查询哪些数据
2. 使用match_metadata工具分别匹配包含这些数据的表结构
3. 对每个表生成相应的SQL查询语句
4. 执行查询并汇总结果
5. 最后计算并给出答案
请确保逐步执行,不要跳过任何步骤。
""",
function_list=[MatchMetadataTool(), ExecuteSQLTool()],
)
def chat(user_query):
print("执行方法chat")
try:
# 运行Agent
messages = [{'role': 'user', 'content': user_query}]
response_generator = agent.run(messages=messages)
# 处理生成器响应
full_response = ''
start = 0
end = 0
for response in response_generator:
# 检查响应类型并适当处理
if isinstance(response, list):
# 如果是列表,提取内容
for item in response:
if isinstance(item, dict) and'content'in item:
full_response = item['content']
end = full_response.__len__()
elif isinstance(item, str):
full_response = item
end = full_response.__len__()
elif isinstance(response, dict) and'content'in response:
full_response = response['content']
end = full_response.__len__()
elif isinstance(response, str):
full_response = response
end = full_response.__len__()
print(f"{full_response[start:end]}", end="")
start = end
print(f"最终结果: {full_response}")
return full_response
except Exception as e:
print(f"执行过程中出错: {str(e)}")
# 这里可以添加重试或更详细的错误处理逻辑
returnf"错误: {str(e)}"
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
print("请提供参数:init或者chat+user_query")
elif args[0] == "init":
print("开始执行方法init")
# 这里可以添加初始化逻辑
elif args[0] == "chat":
print(f"user_query={args[1]}")
chat(args[1])
else:
print(f"未知参数: {args[0]}")
依旧是和LangChain实现的ReAct同样的工具和提示词,只是这次我们使用QwenAgent框架实现。
多数据源RAG检索问题:2016年考生人数和录取人数分别有多少?
运行python agent_service_qwen.py chat '2016年考生人数和录取人数分别有多少?',输出如下:
<think>
好的,用户问的是2016年的考生人数和录取人数。首先我需要确定这两个数据分别存储在哪个表里。可能有一个表记录考生信息,另一个表记录录取情况。我需要先调用match_metadata工具来找到相关的表结构。
先查考生人数,输入“考生人数”看看匹配哪个表。假设返回的是enrollment表,里面有year和candidates字段。然后查录取人数,输入“录取人数”,可能对应admissions表,里面有year和admitted字段。接下来需要分别对这两个表执行SQL查询,筛选出year为2016年的数据。
执行第一个SQL:SELECT candidates FROM enrollment WHERE year = 2016; 得到考生人数。再执行第二个SQL:SELECT admitted FROM admissions WHERE year = 2016; 得到录取人数。然后把结果汇总,给出两个数值。需要确保两个表的年份字段都是2016,并且字段名正确。如果有多个表可能涉及,可能需要进一步确认,但这里假设两次匹配都正确。最后计算结果并呈现给用户。
</think>
调用大模型llama2向量化:{"query":"考生人数"}
自然语言混合检索字段成功,匹配到的元数据信息:['{"表名":"college_entrance_examination","表备注":"考生人数与复读人数信息表,包含字段:高考年份(主键)、考生人数(万人)、复读人数(万人),考生人数是指参加高考的学生的数量,复读人数是指参加高考的复读学生的数量","字段列表":[{"字段名":"examination_year","字段类型":"int","字段备注":"高考年份"},{"字段名":"candidates_count","字段类型":"decimal(10,2)","字段备注":"考生人数(万人)"},{"字段名":"retake_count","字段类型":"decimal(10,2)","字段备注":"复读人数(万人)"}]}']
好的,用户的问题是关于2016年的考生人数和录取人数。首先,我需要确认这两个数据分别存储在哪些表中。之前用户提到过“考生人数”,我调用match_metadata工具后得到了一个表college_entrance_examination,里面包含考生人数和复读人数,但没有录取人数。这说明录取人数可能在另一个表里。
接下来,我需要再次调用match_metadata工具,这次查询“录取人数”。输入关键词后,应该会有对应的表结构返回,比如可能有一个表叫college_admission,包含录取人数字段。假设返回的表结构中有字段admission_count,那么接下来需要分别对这两个表生成SQL查询语句。
对于college_entrance_examination表,SQL语句会是SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016; 这样可以得到2016年的考生人数。然后,针对录取人数所在的表,假设是college_admission,SQL语句可能是SELECT admission_count FROM college_admission WHERE admission_year = 2016; 执行这两个查询后,将结果汇总,就能得到用户需要的两个数据。
需要注意的是,确保两个表的年份字段名称和类型正确,避免因为字段名不同或数据类型不匹配导致查询错误。此外,如果存在多个相关表,需要确认哪一个包含正确的数据,可能需要进一步的信息验证。最后,将查询结果整理后,用清晰的方式呈现给用户,确保数据的准确性和可读性。
</think>
调用大模型llama2向量化:{"query":"录取人数"}
自然语言混合检索字段成功,匹配到的元数据信息:['{"表名":"college_entrance_admission","表备注":"录取人数与普通高校数信息表,包含字段:录取年份(主键)、录取人数(万人)、招生高校数、本科录取人数(万人)、专科录取人数(万人),录取人数是指录取或者招收、招录的学生的数量","字段列表":[{"字段名":"admission_year","字段类型":"int","字段备注":"录取年份"},{"字段名":"admission_count","字段类型":"decimal(10,2)","字段备注":"录取人数(万人)"},{"字段名":"university_count","字段类型":"int","字段备注":"招生高校数"},{"字段名":"undergraduate_admission_count","字段类型":"decimal(10,2)","字段备注":"本科录取人数(万人)"},{"字段名":"specialty_admission_count","字段类型":"decimal(10,2)","字段备注":"专科录取人数(万人)"}]}']
好,用户需要知道2016年的考生人数和录取人数。首先,我需要确定这两个数据分别存储在哪个表里。
先处理考生人数。用户的问题中提到“考生人数”,之前调用match_metadata工具时已经找到了对应的表college_entrance_examination。这个表的字段有examination_year、candidates_count和retake_count。所以考生人数应该从这个表中查询,条件是examination_year等于2016。
然后是录取人数。用户的问题里提到“录取人数”,通过之前的工具调用,发现了表college_entrance_admission。这个表里有admission_year和admission_count,所以录取人数需要从这里查询,同样条件是admission_year等于2016。
接下来需要生成对应的SQL查询语句。对于考生人数,SQL应该是SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016。而录取人数的SQL应该是SELECT admission_count FROM college_entrance_admission WHERE admission_year = 2016。
执行这两个查询后,会得到两个结果,可能都是小数(因为字段类型是decimal)。然后需要将这两个数字组合起来,用万人单位,并且四舍五入到合适的小数位数,可能保留两位小数。
比如,如果查询结果分别是872.21万人和749.61万人,那么最终答案就是考生人数872.21万人,录取人数749.61万人。需要确认年份是否对应,确保两个表的年份字段都正确匹配2016年。
最后,将结果用中文清晰地呈现给用户,说明分别的数据并注明单位。
</think>
执行SQL:SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016
[{"candidates_count":940.0}]
好的,我需要回答用户关于2016年考生人数和录取人数的问题。首先,用户已经通过两次match_metadata调用,分别获取了考生人数所在的表college_entrance_examination和录取人数所在的表college_entrance_admission。
接下来,用户执行了第一个SQL查询,从college_entrance_examination表中获取了2016年的考生人数,结果是940.0万人。现在需要处理录取人数的部分。根据第二个match_metadata的结果,录取人数在college_entrance_admission表的admission_count字段中,且年份字段是admission_year。
因此,我需要构造另一个SQL查询,从college_entrance_admission表中选择admission_count,其中admission_year等于2016。执行这个查询后,将得到录取人数的数据。最后,把两个结果合并,给出用户需要的答案。
</think>
执行SQL:SELECT admission_count FROM college_entrance_admission WHERE admission_year = 2016
[{"admission_count":772.0}]
好的,用户问的是2016年的考生人数和录取人数分别是多少。我需要先找到这两个数据分别存放在哪个表里。
首先,用户的问题涉及到“考生人数”,所以我调用了match_metadata工具,参数是“考生人数”。返回的结果是college_entrance_examination表,里面有考生人数的信息,字段是candidates_count,单位是万人。接着,用户又问到“录取人数”,于是再次调用match_metadata工具,参数是“录取人数”,得到的是college_entrance_admission表,对应的字段是admission_count,同样单位是万人。
接下来,我需要从这两个表中查询2016年的数据。先执行第一个SQL查询,从college_entrance_examination表中选出examination_year等于2016年的考生人数,结果得到940.0万人。然后执行第二个SQL查询,从college_entrance_admission表中选出admission_year等于2016年的录取人数,结果是772.0万人。
确认两个结果都是2016年的数据后,将这两个数值汇总,得到最终答案:2016年考生人数是940万人,录取人数是772万人。需要确保单位正确,并且数据没有错误。这里的数据看起来合理,没有异常值,所以可以确定答案正确。
</think>
2016年的考生人数和录取人数分别为:
- **考生人数**:940万人
- **录取人数**:772万人
数据来源:
- 考生人数来自《考生人数与复读人数信息表》(`college_entrance_examination`)的 `candidates_count` 字段
- 录取人数来自《录取人数与普通高校数信息表》(`college_entrance_admission`)的 `admission_count` 字段最终结果: <think>
好的,用户问的是2016年的考生人数和录取人数分别是多少。我需要先找到这两个数据分别存放在哪个表里。
首先,用户的问题涉及到“考生人数”,所以我调用了match_metadata工具,参数是“考生人数”。返回的结果是college_entrance_examination表,里面有考生人数的信息,字段是candidates_count,单位是万人。接着,用户又问到“录取人数”,于是再次调用match_metadata工具,参数是“录取人数”,得到的是college_entrance_admission表,对应的字段是admission_count,同样单位是万人。
接下来,我需要从这两个表中查询2016年的数据。先执行第一个SQL查询,从college_entrance_examination表中选出examination_year等于2016年的考生人数,结果得到940.0万人。然后执行第二个SQL查询,从college_entrance_admission表中选出admission_year等于2016年的录取人数,结果是772.0万人。
确认两个结果都是2016年的数据后,将这两个数值汇总,得到最终答案:2016年考生人数是940万人,录取人数是772万人。需要确保单位正确,并且数据没有错误。这里的数据看起来合理,没有异常值,所以可以确定答案正确。
</think>
2016年的考生人数和录取人数分别为:
- **考生人数**:940万人
- **录取人数**:772万人
数据来源:
- 考生人数来自《考生人数与复读人数信息表》(`college_entrance_examination`)的 `candidates_count` 字段
- 录取人数来自《录取人数与普通高校数信息表》(`college_entrance_admission`)的 `admission_count` 字段
QwenAgent框架也是和第三章的LangChain框架ReAct流程一样,通过推理、观察、思考、行动完成了多数据源复杂问题的回答。
把复杂问题拆分为简单问题、然后利用我们制定好的流程查出数据,汇总输出!
本文AI Agent的实践过程中,我们了解了es8作为外接知识库检索的基本使用方式(RAG),语义检索、数据查询、大模型分析的基本流程。了解了LangChain和QwenAgent框架的使用。
进一步实践方向:
核心工具与运行时 (Core Tools & Runtimes)
| 工具名称 | 推荐版本 | 说明 |
|---|---|---|
| 操作系统 | macOS 15.6.1 | MacBook pro m1 max 32GB |
| Docker Desktop | 28.3.2 | 本地Docker环境 |
| Python | 3.9 | python实现我们的AI Agent |
| IDE/编辑器 | VS Code / PyCharm | 开发工具 |
项目依赖与服务 (Project Dependencies & Services)
| 服务名称 | 版本 | 端口 | 说明 |
|---|---|---|---|
| Ollama | 0.11.6 | 11434 | 大模型运行环境,运行deepseek-r1:32b、qwen3:32b、llama2 |
| MySQL | 8.4.6 | 3306 | 本地docker部署,'业务知识库' |
| ElasticSearch | 8.19.0 | 9200 | 本地docker部署,支持向量存储、检索 |
为什么使用ES8?
ElasticSearch是一个开源的分布式搜索和分析引擎,主要用于海量文本检索与分析,使用 JSON 格式存储数据,能通过简单的 REST API 进行交互,便于集成。
7.x之前的版本支持text(全文检索字段,支持分词)、keyword(精确值字段,适合过滤、聚合)、numeric、binary、date等传统的基本的数据类型字段。
7.x之后引入dense_vector(向量类型,存储浮点数密集向量如BERT、Word2Vec、Sentence Transformers 生成的向量,维度dims必须提前定义声明,无法修改)。
从8.16版本之后,新增对位类型(bit,0或1)和字节类型(byte,以字节)向量的支持,通过element_type参数指定。默认float(4字节32位浮点数)。数据量较大时可指定byte或bit,牺牲精度以节省存储空间。
支持的量化类型:
7.3-7.4:新增多种向量相似度计算方式(如L2范数即欧几里得距离、点积、余弦相似度、最大内积(适用于负值向量)等)。
8.0:正式支持近似KNN搜索API(基于HNSW算法),显著提升大规模向量检索效率,存储成本大幅下降的同时保持较高的检索准确率。
8.16:扩展支持位(bit)和字节(byte)向量类型,优化存储和计算效率
示例:
{
"mappings":{
"properties":{
"my_vector":{// 字段名称
"type":"dense_vector",
"dims":768,// 和向量化使用的大模型的维度保持一致,不一致插入与检索存在异常
"index":true,// true时支持KNN搜索
"element_type":"byte",// int8一字节
"index_options":{
"type":"int8_hnsw"// int8 量化 + HNSW 算法,必须配置element_type=byte使用
}
}
}
}
}
基于以上优点。
对于Mysql的表结构,我们需要将表字段存储到ES8做向量化,对于用户输入的自然语言,先进行语义检索匹配,索引设计如下:
{
"mappings":{
"properties":{
"table_info":{"type":"keyword"},
"nomic_embedding":{
"type":"dense_vector",
"dims":4096,
"index": True,
"similarity":"cosine",
}
}
}
}
table_info存储表结构信息,nomic_embedding存储表备注向量化信息,指定相似度检索算法为cosine余弦相似度。
本文我们使用LangChain的AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION实现ReAct代理模式,主要含义如下:
| 部分 | 含义 |
|---|---|
| STRUCTURED_CHAT | 支持结构化输入(如 JSON 格式)的聊天式 Agent,能处理复杂参数 |
| ZERO_SHOT | 不依赖于之前的记忆或历史步骤,每一步都独立推理(无“计划”缓存) |
| REACT | 使用 ReAct 框架(Reason + Act),即“思考 → 决策 → 执行 → 观察”循环 |
| DESCRIPTION | 工具通过其 |
特点:
其他类型:
| AgentType | 说明 | 是否支持多工具 | 是否支持结构化输入 | 是否支持 ReAct | 适用场景 |
|---|---|---|---|---|---|
| ZERO_SHOT_REACT_DESCRIPTION | 基础 ReAct Agent,文本形式调用工具 | (仅字符串) | 简单工具链,如计算器+搜索 | ||
| STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION | 支持结构化参数的 ReAct Agent | (JSON 等) | 复杂工具调用(带参数) | ||
| CONVERSATIONAL_REACT_DESCRIPTION | 支持对话记忆的 ReAct Agent,适合聊天机器人 | 客服、对话式助手 | |||
| SELF_ASK_WITH_SEARCH | 使用“自问自答”机制 + 中间子问题分解 | (有限) | 复杂逻辑推理(如维基搜索) | ||
| REACT_DOCSTORE | 基于文档存储的 ReAct(如查找文档) | 文档检索系统 | |||
| OPENAI_FUNCTIONS | 使用 OpenAI 的 | GPT-3.5-turbo / GPT-4 推荐 | |||
| OPENAI_MULTI_FUNCTIONS | 支持一次调用多个函数(OpenAI 扩展) | 高效批量工具调用 |
本文说明:本文因为太长所以分成三篇文章,这是最后一篇,具体业务场景和其他两种方式参见之前文章。
本文作者:Chaiys
本文原载:公众号“木昆子记录AI”