在大模型与智能体日常使用场景层出不穷的今天,作为一名不断学习的信息化数字化从业者,我们可以实践一下AI Agent基本开发流程,理论联系实践,以加深我们对AI Agent的了解。
Chaiys同学基于一个实际业务场景详细描述了AI Agent的开发实践案例,可以用于熟悉RAG、工具调用、ReAct等概念,可以体验LangChain和QwenAgent等AI Agent主流开发框架。
请先查阅前序文章《AI Agent案例实践:三种智能体开发模式详解之一》,以了解业务案例背景、基础开发模式和基于LangChain开发模式。
四、开发实践(基于QwenAgent框架)
前文中,我们使用LangChain框架实现了我们的智能体。下面让我们使用Qwen-Agent实现一版ReAct,体验下流程,具体的业务场景中需要根据需求,调研后决定具体使用哪种方式。
4.1. 什么是QwenAgent?
- LangChain:一个开源框架,专为构建与大语言模型(LLMs)相关的应用设计,通过集成多个API、数据源和工具,助力开发者高效构建智能应用。它更像是AI应用开发的“行业标杆”和“基础设施”,提供了模块化设计(Chains、Agents、Memory)、多模型兼容(OpenAI、HuggingFace等)以及强大的工具调用能力。
- Qwen-Agent:由阿里巴巴团队优化的Agent框架,特别针对中文场景和通义千问(Qwen)系列模型进行了深度优化。它更侧重于具体模型的落地应用,尤其是在处理长文本、复杂推理和多轮对话方面表现出色。
Qwen-Agent 开发框架支持 ReAct 范式
Qwen-Agent 提供了 ReActChat 类来支持 ReAct 推理范式。这允许智能体像人类一样进行思考(Reasoning)、行动(Taking Action)(例如调用工具)和观察(Observing) 结果,并循环此过程直至任务完成 。
- 工作原理:ReActChat 类继承自 FnCallAgent 类,其内部通过 while 循环来管理多轮的工具调用和推理过程 。开发者通常不需要干预这个循环的具体次数。
- 如何使用:你只需要定义好必要的工具(Tools),并将它们分配给智能体。智能体会根据你的查询和系统指令,自行决定是否需要以及如何通过 ReAct 循环来调用这些工具。
关于迭代次数
在 Qwen-Agent 的上下文中,我们通常说的“迭代”指的是 ReAct 循环中模型调用工具并进行推理的轮次。
-
1. 自动控制:这个迭代次数通常不需要开发者手动配置。它主要由底层的大语言模型(LLM)根据当前任务的复杂性、可用工具以及历史上下文来动态决定。模型会自主判断何时拥有了足够的信息来给出最终答案,从而终止循环 。 -
2. 与训练迭代的区别:需要注意的是,这不同于模型训练阶段的超参“迭代次数”(iterations/epochs)。训练迭代指的是整个训练数据集被完整地用于更新模型参数的次数,这在模型微调时需要配置 。但在 Qwen-Agent 框架中,你通常直接使用预训练好的模型,因此不涉及对此类训练迭代次数的配置。
实用建议
- 控制循环:虽然不需要直接设置迭代次数,但你可以通过优化提示词(Prompt)、工具描述的清晰度和准确性,来引导模型更高效地进行推理和工具调用,间接影响循环轮次
接下来我们直接使用QwenAgent框架实现ReAct智能体模式。
4.2.代码实践
具体开发实现如下(agent_service_qwen.py):
import 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()
# 自定义工具类
classMatchMetadataTool(BaseTool):
name = 'match_metadata'
description = '根据输入文本语义匹配表结构,每次返回一个最相关的表结构。对于需要多表查询的问题,需要多次调用此工具。'
defcall(self, params, **kwargs) -> str:
table = semanticService.hybrid_search(params, 1)
returnf"{[t['table_info'] for t in table]}"
classExecuteSQLTool(BaseTool):
name = 'execute_sql'
description = '执行SQL查询并返回结果。输入应为标准SQL语句。注意:可能需要执行多个SQL查询来获取不同表中的数据。'
defcall(self, params, **kwargs) -> str:
ifisinstance(params, str):
jsonObj = json.loads(params)
if'sql'in jsonObj:
params = jsonObj['sql']
if'query'in jsonObj:
params = jsonObj['query']
ifisinstance(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()],
)
defchat(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:
# 检查响应类型并适当处理
ifisinstance(response, list):
# 如果是列表,提取内容
for item in response:
ifisinstance(item, dict) and'content'in item:
full_response = item['content']
end = full_response.__len__()
elifisinstance(item, str):
full_response = item
end = full_response.__len__()
elifisinstance(response, dict) and'content'in response:
full_response = response['content']
end = full_response.__len__()
elifisinstance(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:]
ifnot 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框架实现。
4.3.测试结果
多数据源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框架的使用。
进一步实践方向:
- 场景优化:加入部分心仪的高校数据,每个高校的每个专业在每个省份的招生情况,丰富Agent知识,提供更多更好的服务!
- 检索优化:本文只使用了ES8内部向量类型支持的基本KNN检索,利用余弦相似度计算+分词计算,混合得分后返回,后续可以尝试其他高级检索方式。
- ReAct优化:本文我们的ReAct实践,只使用了LangChain框架的AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION模式,可以实践其他模式效果!
- 流程优化:本文我们的ReAct工具只有语义检索与数据查询两个,没有涉及复杂计算,加入高校信息需要做分段同环比计算,需要加入新的流程,使用新的工具!流程比较简单,作为学习使用,场景丰富后可以进一步优化流程!
- 其他开发模式:本文所列三种开发实践都是基于代码的模式,还可以尝试用Dify、Coze Studio这类低代码工具来配置智能体。
六、附录
6.1. 开发环境
核心工具与运行时 (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部署,支持向量存储、检索 |
6.2. ES8检索
为什么使用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余弦相似度。
6.3. LangChain 常见 AgentType
本文我们使用LangChain的AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION实现ReAct代理模式,主要含义如下:
部分 | 含义 |
STRUCTURED_CHAT | 支持结构化输入(如 JSON 格式)的聊天式 Agent,能处理复杂参数 |
ZERO_SHOT | 不依赖于之前的记忆或历史步骤,每一步都独立推理(无“计划”缓存) |
REACT | 使用 **ReAct 框架**(Reason + Act),即“思考 → 决策 → 执行 → 观察”循环 |
DESCRIPTION | 工具通过其 |
特点:
- ✅ 支持多工具调用
- ✅ 支持带参数的复杂工具(如 SQL 查询带条件)
- ✅ 使用自然语言 + 结构化格式(如 JSON)进行工具调用
- ✅ 适合需要 多步推理 + 参数传递 的场景(如先语义解析再查数据库)
- ✅ 每次决策基于当前上下文重新推理(zero-shot),不依赖预定义流程
其他类型:
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 扩展) | ✅ | ✅ | ✅ | 高效批量工具调用 |