AI Agent案例实践:智能体开发模式详解之三(基于QwenAgent框架)

书接上文: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)根据当前任务的复杂性、可用工具以及历史上下文来动态决定。模型会自主判断何时拥有了足够的信息来给出最终答案,从而终止循环 。
    1. 与训练迭代的区别:需要注意的是,这不同于模型训练阶段的超参"迭代次数"(iterations/epochs)。训练迭代指的是整个训练数据集被完整地用于更新模型参数的次数,这在模型微调时需要配置 。但在 Qwen-Agent 框架中,你通常直接使用预训练好的模型,因此不涉及对此类训练迭代次数的配置。

实用建议

  • 控制循环:虽然不需要直接设置迭代次数,但你可以通过优化提示词(Prompt)、工具描述的清晰度和准确性,来引导模型更高效地进行推理和工具调用,间接影响循环轮次

接下来我们直接使用QwenAgent框架实现ReAct智能体模式。

4.2.代码实践

具体开发实现如下(agent_service_qwen.py):

python 复制代码
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框架实现。

4.3.测试结果

多数据源RAG检索问题:2016年考生人数和录取人数分别有多少?

运行python agent_service_qwen.py chat '2016年考生人数和录取人数分别有多少?',输出如下:

swift 复制代码
<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)向量类型,优化存储和计算效率

示例:

ruby 复制代码
{
"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做向量化,对于用户输入的自然语言,先进行语义检索匹配,索引设计如下:

python 复制代码
{
  "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 扩展) 高效批量工具调用

本文说明:本文因为太长所以分成三篇文章,这是最后一篇,具体业务场景和其他两种方式参见之前文章。

本文作者:Chaiys

本文原载:公众号"木昆子记录AI"

相关推荐
驭白.3 小时前
不止于自动化:新能源汽车智造的数字基座如何搭建?
大数据·人工智能·自动化·汽车·数字化转型·制造业
superman超哥3 小时前
Rust 内部可变性模式:突破借用规则的受控机制
开发语言·后端·rust·rust内部可变性·借用规则·受控机制
企业智能研究3 小时前
什么是数据治理?数据治理对企业有什么用?
大数据·人工智能·数据分析·agent
阿里云大数据AI技术4 小时前
面向 Interleaved Thinking 的大模型 Agent 蒸馏实践
人工智能
柒.梧.4 小时前
Spring核心知识全解析:从入门实战到进阶
java·后端·spring
AI Echoes4 小时前
LangChain 非分割类型的文档转换器使用技巧
人工智能·python·langchain·prompt·agent
乌日尼乐4 小时前
【Java基础整理】Java字符串处理,String、StringBuffer、StringBuilder
java·后端
哔哔龙4 小时前
LangChain核心组件可用工具
人工智能
qwepoilkjasd4 小时前
DMC发送M-SEARCH请求,DMR响应流程
后端
全栈独立开发者4 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring