书接上文:Agent案例实践:三种智能体开发模式详解之一(手写代码)
三、开发实践(基于LangChain框架)
上述高考信息查询的实现,我们的流程:语义匹配表元数据-->大模型根据提示词生成SQL-->查询数据-->大模型根据查询的数据分析,只是利用了大模型基本能力和接口,手写了一段流程实现,下面我们使用LangChain框架做进一步优化,同时体验一下qwen3:32b的大模型效果。
3.1. 什么是LangChain?
LangChain 是一个开源框架,主要用来 让大语言模型(LLM)能够更好地和外部世界交互,从而不只是“对话生成器”,而是变成可以调用工具、处理数据、接入知识库、执行任务的“智能体”。
核心目标
大模型虽然很强,但有两个局限:
- 知识受限:只能使用训练时的数据,无法实时查询最新信息。
- 功能受限:不能直接调用数据库、API、文件系统等外部工具。
LangChain 就是为了解决这两个问题:它提供了一套组件和机制,让 LLM 可以被 组合、扩展,并与外部系统交互。
LangChain主要功能
- Prompt 管理
提供更灵活的 Prompt 模板系统,可以动态构建提示,适应不同任务。
- 链式调用(Chains)
允许把多个步骤串起来,比如:先调用模型生成 SQL → 再执行数据库查询 → 把结果返回给模型继续总结。
- 工具调用(Tools / Agents)
让模型学会决定什么时候调用外部工具,比如:
- 调用 搜索引擎 获取最新新闻
- 调用 Python 代码执行器 做数学运算
- 调用 数据库接口 查询公司内部数据
-
知识检索(Retrieval / RAG)
集成向量数据库(如 Pinecone、Weaviate、Chroma),把企业文档或本地知识库接入,让 LLM 可以基于外部知识回答问题。
3.2. 顺序链SequentialChain调用
在LangChain框架中,SequentialChain 是一种固定顺序执行的链式结构。多个 Chain 按照预定义的顺序依次执行,前一个的输出作为后一个的输入。
- 所有步骤是预先设计好的。
- 没有动态决策能力。
核心特点:
- 步骤明确:如“语义检索 → 生成/执行SQL → 查数分析”。
- 输入输出清晰:每个 Chain 接收上一个的输出。
- 支持 SimpleSequentialChain(单输入输出)和更复杂的 MultipleChain。
使用 SequentialChain :
- 1. 步骤拆分:
-
- 将流程拆分为三个明确的步骤:获取表结构 → 生成/执行 SQL → 分析结果
- 使用 TransformChain 处理自定义函数操作
- 使用 LLMChain 处理需要 LLM 参与的步骤
- 2. 保留原有功能:
-
- 保持原有的服务初始化(QueryService、SemanticService 等)
- 保持相同的流式输出回调配置
- 维持原有的命令行参数处理逻辑
- 3. 流程清晰明确:
-
- 第一步:通过语义服务获取表结构
- 第二步:生成 SQL 并执行查询
- 第三步:对查询结果进行分析并生成中文分析
3.2.1. 代码实践
具体开发实现如下(agent_service_seq.py):
import sys
import re
from langchain.chains import LLMChain, SequentialChain, TransformChain
from langchain.prompts import PromptTemplate
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain_ollama import OllamaLLM
from api_service import QueryService, SemanticServce, AnalysisService
# 初始化服务
queryService = QueryService()
semanticService = SemanticServce()
analysisService = AnalysisService()
# 初始化 Ollama
llm = OllamaLLM(
model="qwen3:32b",
callbacks=[StreamingStdOutCallbackHandler()],
)
# 第一步:语义匹配表结构
def get_table_schema(inputs: dict) -> dict:
user_query = inputs["user_query"]
table = semanticService.hybrid_search(user_query, 1)
table_list = [t["table_info"] for t in table]
return {"table_schema": table_list}
def find_sql(str):
# 只提取SQL语句内容(不包括标记)
pattern = r'```sql\s*([\s\S]*?)\s*;?\s*```'
match = re.search(pattern, str, re.DOTALL)
ifmatch:
sql_content = match.group(1).strip()
return sql_content
return ";"
# 第二步:生成并执行 SQL
def execute_sql(inputs: dict) -> dict:
table_schema = inputs["table_schema"]
user_query = inputs["user_query"]
# 生成 SQL 的提示模板
sql_prompt = PromptTemplate(
input_variables=["table_schema", "user_query"],
template="基于以下表结构:{table_schema}\n请根据用户查询生成SQL语句:{user_query},markdown格式返回最终SQL"
)
sql_chain = LLMChain(llm=llm, prompt=sql_prompt)
generated_sql = sql_chain.run({
"table_schema": table_schema,
"user_query": user_query
})
# 提取 SQL
generated_sql = find_sql(generated_sql)
# 执行 SQL
sql_result = queryService.query_with_column(generated_sql)
return {"sql_result": sql_result, "generated_sql": generated_sql}
# 第三步:分析结果
analysis_prompt = PromptTemplate(
input_variables=["user_query", "sql_result", "generated_sql", "table_schema"],
template="用户查询:{user_query}\n生成的SQL:{generated_sql}\n查询结果:{sql_result}\n请用中文分析结果:"
)
analysis_chain = LLMChain(
llm=llm,
prompt=analysis_prompt,
output_key="analysis_result"
)
# 构建顺序链
overall_chain = SequentialChain(
chains=[
TransformChain(
input_variables=["user_query"],
output_variables=["table_schema"],
transform=get_table_schema
),
TransformChain(
input_variables=["user_query", "table_schema"],
output_variables=["sql_result", "generated_sql"],
transform=execute_sql
),
analysis_chain
],
input_variables=["user_query"],
output_variables=["analysis_result"],
verbose=True
)
# metadata init
def init():
print("开始执行方法init")
# 新的顺序链调用
def chat(user_query):
print("执行方法chat")
result = overall_chain({"user_query": user_query})
print(result["analysis_result"])
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
print("请提供参数:init或者chat+user_query")
elif args[0] == "init":
init()
elif args[0] == "chat":
print(f"user_query={args[1]}")
chat(args[1])
else:
print(f"未知参数: {args[0]}")
3.2.2. 测试结果
问题:2016年考生人数有多少?
运行python agent_service_seq chat '2016年考生人数有多少?',输出结果如下:
> Entering new SequentialChain chain...
调用大模型llama2向量化:'2016年考生人数有多少?'自然语言混合检索字段成功,匹配到的元数据信息:[{'id': 'IH221JgBJAZjJoUFbd3E', 'table_info': '{"表名": "college_entrance_examination", "表备注": "考生人数与复读人数信息表,包含字段:高考年份(主键)、考生...省略若干字段信息..."}]}']}]/Users/chaiys/Software/Work/ai-all/CollegeEntrance/agent_service_seq.py:49: LangChainDeprecationWarning: The method `Chain.run` was deprecated in langchain 0.1.0 and will be removed in 1.0. Use :meth:`~invoke` instead. generated_sql = sql_chain.run({
好的,我需要根据用户的问题生成对应的SQL查询语句。用户问的是“2016年考生人数有多少?”,首先,我要确定问题中的关键信息。年份是2016年,需要查询的是考生人数,对应的表是college_entrance_examination。
接下来,查看表结构。表中有三个字段:examination_year(int类型,主键),candidates_count(decimal(10,2),考生人数),retake_count(decimal(10,2),复读人数)。用户的问题关注的是考生人数,所以应该选择candidates_count字段。
然后,确定条件。因为要查询2016年的数据,所以需要where子句过滤examination_year等于2016。由于examination_year是主键,所以查询应该能返回唯一的记录。
现在,构造SQL语句。基本结构是SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016;。
需要注意字段名和表名是否正确,以及条件是否正确。确认无误后,就可以生成最终的SQL了。
```
sqlSELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016;
```
执行SQL:SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016
> Finished chain.
好的,用户问的是2016年的考生人数,我需要先生成对应的SQL查询。首先,表名应该是college_entrance_examination,然后字段有examination_year和candidates_count。用户的问题是要找出2016年的考生人数,所以SQL应该是SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016。执行这个查询后,结果返回了940.0。
这时候需要确认数据是否正确,因为940这个数字看起来有点小,可能是不是百万或者万的单位?不过用户的数据可能已经处理过了,所以应该直接使用结果。接下来用中文分析结果,说明2016年的考生人数是940万,但需要确认单位是否正确。如果数据表里的单位是百万,那结果就是9400万,但这里显示的是940.0,可能用户的数据是已经处理成百万单位的。所以回答时应该指出数据可能以百万为单位,并建议确认单位是否正确。同时,保持回答简洁明了,让用户清楚知道结果。
根据查询结果,2016年中国高考考生人数为940万人。需要注意的是,这个数据可能已经过官方统计调整,实际人数可能因统计口径或数据来源存在细微差异。建议结合当年教育部发布的权威数据进行交叉验证。
我们看到,SequentialChain和第二章的手写智能体流程一致,链式调用工具,先是调用语义检索工具匹配到元数据,然后构造SQL,最后调用数据查询工具查询到2016年的考生人数数据,输出正确!
3.3. 当流程不再固定:SequentialChain 的局限性
上一节中,我们实践了 SequentialChain的使用,优化了手写Workflow流程,它允许我们将多个独立的 Chain 按照预设的顺序依次执行,每个步骤的输出作为下一步的输入。这种方式非常适合处理流程固定、逻辑清晰的任务,例如:2016年考生人数有多少?,先语义检索元数据,再生成/执行SQL查询数据,最后分析输出。
然而,在真实的应用场景中,任务往往更加复杂且不可预测。我们可能需要根据中间结果动态决定下一步的操作,比如:当我们问到'2016年考生人数和录取人数分别有多少'时,由于我们的RAG语义检索元数据只匹配到其中一个考生人数表或者一个录取人数表(因为元数据检索只返回一个最匹配的结构,但是考生人数和录取人数存在于两个表中),LangChain顺序链调用实际问答输出结果大致是:根据2016年数据,全国普通高校录取总人数为772万人,其中本科录取405万人,专科录取367万人。注:当前数据表中未包含考生人数字段,仅能提供录取人数信息,考生人数可能需要通过其他数据源获取),很明显只返回了录取人数,遗漏了考生人数。
我们需要智能体可以正确的理解我们的问题并对复杂提问进行拆分,多轮查询汇总结果。即复杂问题简单化,简单问题流程化。由此,我们引出Agent,看看如何处理与解决复杂问题!
3.4 动态决策:Agent智能体调用
Agent是一种能够根据用户输入动态决定“下一步做什么”的智能组件。它使用语言模型(LLM)作为“大脑”,通过推理选择工具(Tools)来完成任务。
特点:
- Agent 不是固定的流程,而是动态决策。
- 它会根据当前状态决定调用哪个工具、是否需要进一步推理等。
核心组成:
- LLM(大语言模型):作为决策引擎。
- Tools(工具):可调用的函数或 API,如搜索、计算、数据库查询等。
- Agent Executor:执行 Agent 决策循环,控制流程。
3.4.1. Agent如何知道调用哪个工具
-
核心机制:Prompt 驱动的决策
LangChain 的 Agent 并不是靠硬编码规则来选择工具,而是依赖 精心设计的 Prompt,让大模型自己“推理”出下一步动作。
这个 Prompt 通常包含以下关键信息:
核心工具列表:每个工具的名称、功能描述、参数格式(JSON Schema)
当前对话历史:用户说了什么?之前做了哪些操作?
Agent的思考格式:规定输入输出格式
-
工作流程 LangChain Agent 的运行是一个循环过程,称为 ReAct 模式(Reason + Act)
3.4.2. 什么是ReAct?
ReAct(Reasoning and Action)核心思想就是通过思维链的方式,引导模型将复杂问题进行拆分,一步一步地进行推理(Reasoning)和行动(Action),同时还引入了观察(Observation)环节,在每次执行(Action)之后,都会先观察(Observation)当前现状,然后再进行下一步的推理(Reason)。
工作流程
- 1. 输入数据:接收用户输入或环境数据。
- 2. 推理阶段:分析输入数据,生成决策和计划。
- 3. 行动阶段:根据决策执行具体操作。
- 4. 观察阶段:监控操作结果,收集反馈数据。
- 5. 循环迭代:根据反馈数据调整推理和行动,持续优化结果。
可以看到,这种分析流程,正是我们解决复杂问题所需要的!
当我们问到'2016年考生人数和录取人数分别有多少',因为考生人数在一个表(这里我们用表模拟不同数据来源),录取人数在另一个表,而我们的小助手根据语义匹配每次只能返回一个表结构,只能查询一个考生人数或者一个录取人数,无法得到多个值。这里就需要Agent自动分解问题与多轮查询汇总结果,找到最终答案!
3.4.3. 总体架构
使用LangChain Agent改造我们上述顺序链SequentialChain调用模式,最终架构如下:
3.4.4. 代码实践
这里我们使用Agent实现,告诉Agent我们需要拆分问题,多次迭代。
优化具体代码如下(agent_service_react.py):
import sys
from langchain.agents import Tool
from langchain.agents import initialize_agent, AgentType
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
from api_service import QueryService, SemanticServce, AnalysisService
# 初始化服务
queryService = QueryService()
semanticService = SemanticServce()
analysisService = AnalysisService()
# 初始化 Ollama
llm = OllamaLLM(
model="qwen3:32b",
callbacks=[StreamingStdOutCallbackHandler()]
)
# 自定义工具
def match_metadata(user_query: str) -> str:
"""语义匹配表结构,可多次调用"""
table = semanticService.hybrid_search(user_query, 1)
table_list = [t["table_info"] for t in table]
returnf"{table_list}"
def execute_sql(query: str) -> str:
"""执行SQL查询"""
return queryService.query_with_column(query)
# 创建工具
semantic_tool = Tool(
name="match_metadata",
func=match_metadata,
description="根据输入文本语义匹配表结构,每次返回一个最相关的表结构。对于需要多表查询的问题,需要多次调用此工具。"
)
sql_tool = Tool(
name="execute_sql",
func=execute_sql,
description="执行SQL查询并返回结果。输入应为标准SQL语句。注意:可能需要执行多个SQL查询来获取不同表中的数据。"
)
# 初始化Agent
agent = initialize_agent(
tools=[semantic_tool, sql_tool],
llm=llm,
agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
handle_parsing_errors=True,
max_iterations=10, # 增加最大迭代次数以支持多步查询
early_stopping_method="generate"
)
def chat(user_query):
print("执行方法chat")
# 更明确的指令
enhanced_query = f"""
问题:{user_query}
请特别注意:这个问题可能需要从多个表中查询数据。
1. 首先确定需要查询哪些数据
2. 使用match_metadata工具分别匹配包含这些数据的表结构
3. 对每个表生成相应的SQL查询语句
4. 执行查询并汇总结果
5. 最后计算并给出答案
请确保逐步执行,不要跳过任何步骤。
"""
try:
result = agent.run(enhanced_query)
print(f"\n最终结果: {result}")
except Exception as e:
print(f"执行过程中出错: {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]}")
通过Agent动态决策,我们使用提示词告诉大模型,我们的问题属于复杂问题可能需要从多个表中查询数据,让Agent决策是否需要拆分与迭代查询。
3.4.5. 测试结果
问题:2016年考生人数和录取人数分别有多少?
输出结果:
> Entering new AgentExecutor chain...
<think>
好的,我现在要解决用户的问题:“2016年考生人数和录取人数分别有多少?”首先,我需要确定需要查询哪些数据。考生人数和录取人数可能分别存储在不同的表中,或者同一个表里的不同字段。因此,第一步是使用match_metadata工具来找到相关的表结构。
首先,我应该调用match_metadata工具,输入“考生人数”来找到可能包含该信息的表。然后,再调用一次match_metadata,输入“录取人数”来找到对应的表。假设系统返回了两个不同的表,比如enrollment和admission,那么接下来需要为每个表生成SQL查询语句,筛选出2016年的数据。
对于考生人数,可能需要查询enrollment表中的总人数,而录取人数可能在admission表中。执行这两个SQL查询后,获取到相应的数据,然后汇总结果,确保两个数据都是2016年的,最后将结果返回给用户。需要注意的是,如果表中没有年份字段,可能需要进一步确认数据的时间范围。但根据问题中的年份,假设表结构中有year字段,可以正确筛选数据。
</think>
Thought: 需要确定“考生人数”和“录取人数”分别存储在哪些表中,首先调用match_metadata工具匹配相关表结构。
Action:
```
{
"action": "match_metadata",
"action_input": "考生人数"
}
```
调用大模型llama2向量化:考生人数
自然语言混合检索字段成功,匹配到的元数据信息:[{'id': 'IH221JgBJAZjJoUFbd3E', 'table_info': '{"表名": "college_entrance_examination", "表备注": "考生人数与复读人数信息表,包含字段:高考年份(主键)、考生人数(万人)、复读人数(万人),...省略若干字段信息..."}]}', 'semantic_score': 0.6878319, 'keyword_score': 2.2218122, 'combined_score': 1.14802599, 'highlight': ['{"表名": "college_entrance_examination", "表备注": "<em>考生人数</em>与复读<em>人数</em>信息表,包含字段:...省略..."}]}']}]
Observation: ['{"表名": "college_entrance_examination", "表备注": "考生人数与复读人数信息表,包含字段:高考...省略若干字段信息..."}]}']
Thought: 已找到考生人数对应的表`college_entrance_examination`,需要继续匹配"录取人数"对应的表结构。
Action:
```
{
"action": "match_metadata",
"action_input": "录取人数"
}
```
调用大模型llama2向量化:录取人数
自然语言混合检索字段成功,匹配到的元数据信息:[{'id': 'H3221JgBJAZjJoUFbd02', 'table_info': '{"表名": "college_entrance_admission", "表备注": "录取人数与普通高校数信息表,包含字段:录取年份(主键)、录取人数(万人)、招生高校数、本科...省略若干字段信息..."}]}', 'semantic_score': 0.6326351, 'keyword_score': 1.9648268, 'combined_score': 1.03229261, 'highlight': ['{"表名": "college_entrance_admission", "表备注": "<em>录取人数</em>与普通高校数信息表,包含字段:录取年份(主键)、<em>录取人数</em>(万人)、招生高校...省略若干字段信息..."}]}']}]
Observation: ['{"表名": "college_entrance_admission", "表备注": "录取人数与普通高校数信息表,包含字段:录取年份(主键)、录取人数(万人)、招生...省略若干字段信息..."}]}']
Thought: 已获取两个表的结构,现在需要分别生成SQL查询语句并执行。首先查询`college_entrance_examination`表中2016年的考生人数,再查询`college_entrance_admission`表中2016年的录取人数。
Action:
```
{
"action": "execute_sql",
"action_input": "SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016"
}
```
<think>
执行SQL:SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016
Observation: [{'candidates_count': 940.0}]
Thought:<think>
好的,现在用户的问题是要找出2016年的考生人数和录取人数。我之前已经通过match_metadata工具找到了两个相关的表:一个是college_entrance_examination,包含考生人数;另一个是college_entrance_admission,包含录取人数。接下来,我需要分别对这两个表执行SQL查询。
首先,针对考生人数,我执行了SELECT candidates_count FROM college_entrance_examination WHERE examination_year = 2016,结果返回了940.0万。这一步看起来没问题,数据正确。
接下来,我需要查询录取人数,也就是在college_entrance_admission表中查找admission_count,条件是admission_year = 2016。这时候应该执行相应的SQL语句。根据之前的工具调用结果,这个表的结构中有admission_year和admission_count字段,所以SQL语句应该是正确的。
现在需要执行第二个SQL查询,然后根据结果给出最终答案。确保两次查询的结果都是2016年的数据,然后正确汇总。
</think>
```
{
"action": "execute_sql",
"action_input": "SELECT admission_count FROM college_entrance_admission WHERE admission_year = 2016"
}
```
执行SQL:SELECT admission_count FROM college_entrance_admission WHERE admission_year = 2016
Observation: [{'admission_count': 772.0}]
Thought:<think>
Okay, let me go through the steps here. The user asked for the number of candidates and admissions in2016. I first used the match_metadata tool to find the relevant tables. For candidates, it was the college_entrance_examination table, andfor admissions, it was college_entrance_admission. Then I executed SQL queries on both tables for the year 2016. The first query gave 940.0 candidates, and the second gave 772.0 admissions. Now I need to present these numbers as the final answer. I should make sure the units are correct, which are in ten thousands as per the table remarks. So the answer should state 940.0 million candidates and772.0 million admissions. I'll format it clearly to match the user's request.
</think>
Action:
```
{
"action": "Final Answer",
"action_input": "2016年考生人数为940.0万人,录取人数为772.0万人。"
}
```
> Finished chain.
最终结果: 2016年考生人数为940.0万人,录取人数为772.0万人。
可以看到,Agent通过ReAct工作流程迭代,根据提示词自我决策拆分问题为考生人数和录取人数,然后分别调用语义检索、查询数据工具,得出中间结果,最终汇总分析输出正确结果!
3.5. 执行者到决策者:SequentialChain VS Agent
通过前文的实践,我们可以感受到SequentialChain和Agent的区别,SequentialChain像一个执行者,按照既定流程去执行,Agent像一个决策者,动态决策执行:
| 维度 | SequentialChain | Agent |
|---|---|---|
| 流程控制 | 预设、静态 | 动态、模型驱动 |
| 工具调用 | 固定顺序 | 按需选择 |
| 适用场景 | 确定性任务 | 开放性复杂问题 |
本文说明:本文因为太长所以分成三篇文章,这是第二篇,还有最后一篇是基于QwenAgent框架的实践,欢迎继续阅读。
本文作者:Chaiys
本文原载:公众号“木昆子记录AI”