LangChain 实践3 5无Function Call的结构化通用Agent 6Function Call 智能工具助手
项目5 无Function Call的结构化通用Agent
知识点:结构化Agent、Prompt工程、不支持Function Call模型适配、自定义工具
功能
- 自己写 Prompt 约束格式:Action:xxx Action Input:xxx
- 不用模型原生Function Call,纯提示词实现工具调用
- 接入:计算器、时间查询、本地文档检索工具
落地:结构化Agent不靠模型,靠Prompt实现Agent。
项目拆解
核心定位:纯 Prompt 工程实现 Agent 工具调用,零模型原生函数调用能力。
- 阶段一:项目基础协议定义(前置基建,无代码)
- 步骤 1:定义 Agent 结构化交互协议(项目核心标准)
- 实践:统一模型输出字段、工具调用格式、问答分支规则
- 配套理论:无 Function Call Agent 的结构化解析原理
- 阶段二:Prompt 工程实现(Agent 大脑规则层)
- 步骤 2:手写完整版结构化约束 System Prompt
- 实践:编写角色规则、工具清单、判断逻辑、正负样例、输出规约
- 配套理论:Prompt 工程如何替代原生 Function Call 能力
- 阶段三:代码基建(核心底座,Python)
- 步骤 3:搭建项目基础代码结构,初始化全局配置
- 实践:创建项目文件结构、导入依赖、定义全局常量
- 配套理论:模块化 Agent 工程的分层设计思想
- 阶段四:核心能力开发(工具层)
- 步骤 4:开发 3 个自定义原生工具
- 实践:逐一对接实现 calculator、time_query、doc_search 工具函数
- 配套理论:自定义工具的入参、出参标准化设计规范
- 阶段五:Agent 核心逻辑(解析 + 调度层)
- 步骤 5:开发文本格式解析器
- 实践:代码解析模型输出,提取 Thought / Action / Action Input
- 配套理论:结构化输出的正则匹配解析原理
- 步骤 6:开发工具路由调度器
- 实践:根据解析出的 Action 名称,自动分发执行对应工具
- 配套理论:Agent 工具调度的路由机制
- 阶段六:Agent 闭环能力(多轮迭代)
- 步骤 7:实现单轮 Agent 调用链路
- 实践:用户提问→模型推理→工具调用→结果返回
- 步骤 8:实现多轮 Agent 思考闭环
- 实践:工具结果回传模型,支持多次连续调用工具,直至问题解决
- 配套理论:Agent ReAct 思考迭代框架核心逻辑
- 阶段七:测试落地
- 步骤 9:全场景功能测试、异常兼容优化
- 实践:测试计算、时间、文档检索、无需工具直接回答、格式异常场景
本项目属于独立可复用的工具调度模块。
整体层级关系:
- 上层:大模型负责思考决策、输出规范格式文本
- 中层:咱们写的解析 + 调度逻辑,拆解指令、匹配工具
- 底层:计算器、文档查询这类具体功能函数
本项目承接模型指令,落地实际操作,是完整智能体里的执行环节。
1 项目基础协议
放弃模型原生函数调用接口,依靠约定文本格式做调用标识,程序端解析文本完成工具触发,实现等效 Agent 工具调用能力。
1.1 应答分支规则
- 无需调用工具:直接输出最终答案,不出现指定格式字段
- 需要调用工具:严格遵循固定三段式格式输出,字段顺序不可调换
1.2 标准输出格式
Thought:思考判断内容 Action:工具标识名 Action Input:工具传入参数1.3 工具名册与使用规范
| 工具标识 | 功能用途 | 入参要求 |
|---|---|---|
| calculator | 数学运算计算 | 填写完整数学表达式 |
| time_query | 获取当前系统时间 | 无参数,填空即可 |
| doc_search | 本地文档关键词检索 | 填写检索关键词 |
1.4 格式约束细则
- 字段名称固定大小写、文字不可修改删减
- 思考内容简洁说明调用工具的原因
- 参数严格匹配对应工具格式,不额外附加多余描述
2 结构化约束 System Prompt
通过提示词强制约束模型输出范式、决策逻辑与工具使用规则,替代原生 Function Call 接口,让模型按约定格式产出可解析调用指令。
2.1 完整 Prompt 文案
你是结构化智能Agent,无法使用模型原生函数调用能力,仅按既定格式完成交互作答。 可用工具列表:1.calculator:执行数学运算,入参填写标准数学表达式2.time_query:查询当前系统时间,无需传入参数3.doc_search:检索本地文档内容,入参填写检索关键词 作答规则:1.简单问题无需调用工具,直接给出最终答案即可2.涉及计算、查时间、查文档的问题,必须严格按下方固定格式输出,不得增减内容、更改字段顺序 标准调用格式: Thought:简述调用工具的原因 Action:填写对应工具标识 Action Input:填写对应所需参数 参考示例: 示例1调用计算 Thought:需要计算数值结果,调用计算器工具 Action:calculator Action Input:12+36*2示例2查询时间 Thought:需要获取当前时间,调用时间查询工具 Action:time_query Action Input:示例3文档检索 Thought:需要查找相关文档信息,调用文档检索工具 Action:doc_search Action Input:项目开发规范2.2 实操要点
- 文案明确区分直接作答、工具调用两种场景
- 绑定前期约定的工具名与入参规范,保证前后统一
- 附上实操样例,降低模型格式出错概率
3 项目基础代码结构
采用分层模块化架构,拆分规则、工具、解析、调度模块,降低耦合,便于后续迭代扩展。
3.1 规划目录文件结构
agent_project/├── main.py# 程序入口、对话主逻辑├── tools.py# 计算器/时间/文档检索工具函数└── parser.py# 模型输出格式解析逻辑3.2 初始化各基础文件
本次基础功能无需额外第三方库,仅使用 Python 内置模块即可开发,无需额外安装包。
简单问题:文字释义、词语理解这类属于模型自身知识库内容,不属于原生函数调用,无需动用工具,直接作答即可。
内置知识库 vs 联网搜索 边界总结
内置知识库(不触发 invoke,低消耗)
- 模型训练时录入的存量知识,本地直接推理作答适用:词语释义、常识典故、基础理论、历史固定内容、文本分析、常规问答
联网搜索 / 外部工具(触发 invoke,额外耗资源)
- 向外请求外部数据、实时内容、本地文件、运算服务适用:实时时间、最新资讯、动态数据、数学计算、文档查询、当下变动信息
核心分界
- 信息固定不变→用内置知识
- 信息实时可变 / 外部独有→走调用检索
4 自定义工具函数
工具单独封装成独立单元,统一输入输出形式,和调度逻辑拆分,改动互不影响。
4.1 明确文件与整体思路
- 操作文件:tools.py
- 整体规划:分别编写三个工具方法,最后用字典建立工具名和方法的对应关系
4.2 逐个梳理工具开发要点
- 计算器工具
- 接收参数:数学表达式字符串
- 核心逻辑:解析表达式算出结果
- 容错处理:捕获计算出错的情况,返回异常提示
- 时间查询工具
- 接收参数:无实际有效参数
- 核心逻辑:读取系统当前时间,规范日期时间展示格式
- 返回内容:格式化后的时间文本
- 本地文档检索工具
- 接收参数:检索关键词
- 核心逻辑:内置模拟文档数据,根据关键词匹配内容
- 判定分支:匹配到则返回对应内容,无匹配则给出未查询到提示
4.3 构建工具映射关系
定义映射容器,把之前约定的 action 标识名,一一对应绑定到各自工具方法,后续调度时直接通过名称就能调用对应功能。
# 工具映射表TOOL_MAP={"calculator":compute,"time_query":get_time,"doc_search":search_document}5 文本格式解析器
拆分模型返回文本,提取 Thought、Action、Input 三段内容,为后续工具调度提供有效数据。
5.1 核心思路
- 按固定字段标识切割文本
- 分别取出思考内容、工具名、入参
- 校验字段完整性,异常则判定无需调用工具
5.2 关键操作要点
- 匹配Thought:、Action:、Action Input:前缀拆分内容
- 去除首尾多余空格换行,规整数据
- 解析成功返回三段数据,解析失败返回空标识
6 工具路由调度器
承接解析后的指令,匹配对应工具函数,完成调用并返回执行结果。
执行步骤
- 调用解析器,拆分出思考内容、工具标识、传入参数
- 判断工具标识是否为空,无调用指令则仅返回思考信息
- 检索工具映射表,校验当前工具是否合法可用
- 匹配到对应工具函数,传入参数执行运算查询
- 汇总思考过程与工具执行结果,统一对外输出
标准格式测试输入文本
Thought:查询项目相关说明 Action:doc_search Action Input:项目介绍核心代码
fromparserimportparserfromtoolsimportTOOL_MAPif__name__=='__main__':test_input="""Thought: 查询项目相关说明 Action: doc_search Action Input: 项目介绍"""# 调用解析器,拆分出思考内容、工具标识、传入参数input_parser=parser(test_input)print(input_parser)res=''# 判断工具标识是否为空,无调用指令则仅返回思考信息ifinput_parser['action']=='':res+=f"thought:{input_parser['thought']}"else:# 检索工具映射表,校验当前工具是否合法可用ifinput_parser['action']notinTOOL_MAP:print('当前工具不可用')else:# 匹配到对应工具函数,传入参数执行运算查询tool=TOOL_MAP[input_parser['action']]# 汇总思考过程与工具执行结果,统一对外输出output=tool(input_parser['action_input'])res+=f"thought:{input_parser['thought']}\n output:{output}"print(res)运行结果
{'thought':'查询项目相关说明','action':'doc_search','action_input':'项目介绍'}thought:查询项目相关说明 output:这是一个无FunctionCall的结构化Agent7 单轮 Agent 调用链路
完整流转:用户提问 → 模型推理生成结构化指令 → 解析拆解指令 → 路由匹配工具执行 → 整合结果反馈。
执行步骤
- 定义接收用户问题的入口
- 编写模拟模型逻辑,依据问题内容,产出规范格式的思考、动作、参数文本
- 调用原有解析模块,拆分出思考内容、工具标识、入参
- 走工具调度逻辑,判断是否调用工具并执行
- 拼接信息,输出最终答复
核心代码
(先用 mock 模拟输出,现阶段只模拟推理结果格式,暂不接入真实大模型接口)
fromparserimportparserfromtoolsimportTOOL_MAPdefinvoke(input):print(input)# 编写模拟模型逻辑,依据问题内容,产出规范格式的思考、动作、参数文本ifinput.find("计算")!=-1:test_input="""Thought: 1+1结果是什么 Action: calculator Action Input: 1+1"""elifinput.find("查询")!=-1:test_input="""Thought: 查询项目相关说明 Action: doc_search Action Input: 项目介绍"""elifinput.find("时间")!=-1:test_input="""Thought: 现在是什么时间 Action: time_query Action Input: """returntest_inputif__name__=='__main__':# 定义接收用户问题的入口llm_output=invoke("现在是什么时间")# 调用解析器,拆分出思考内容、工具标识、传入参数input_parser=parser(llm_output)res=''# 判断工具标识是否为空,无调用指令则仅返回思考信息ifinput_parser['action']=='':res+=f"thought:{input_parser['thought']}"else:# 检索工具映射表,校验当前工具是否合法可用ifinput_parser['action']notinTOOL_MAP:print('当前工具不可用')else:# 匹配到对应工具函数,传入参数执行运算查询tool=TOOL_MAP[input_parser['action']]# 汇总思考过程与工具执行结果,统一对外输出output=tool(input_parser['action_input'])res+=f"thought:{input_parser['thought']}\n output:{output}"print(res)8 ReAct 多轮思考闭环
8.1 ReAct
ReAct = Reason(推理思考)+ Act(行动调用工具)是 Agent 经典迭代框架,边思考边行动,循环往复解决问题。
8.2 核心运作流程
- 思考(Reason)
- 模型分析问题 + 已有信息,判断是否需要调用工具、用什么工具
- → 对应你代码里的 invoke 函数,生成结构化指令
- 行动(Act)
- 解析指令 → 匹配工具 → 执行函数 → 拿到结果
- → 对应你的 parser + 工具调度逻辑
- 复盘(反馈)
- 把工具执行结果回传给模型,让模型重新判断问题是否解决
- 循环(迭代)
- 未解决 → 继续思考 + 调用工具
- 已解决 → 停止循环,给出最终答案
核心规则
工具结果回传给模型,循环判断是否继续调用工具,直到问题办结。
8.3 实操分步思路
- 定义对话上下文变量
- 用来存放用户问题、每一轮的思考、工具执行结果,持续传给模型
- 用循环包裹单轮执行逻辑
- 让 Agent 可以自动重复:思考→行动→复盘,而不是只跑一次
- 每轮结束拼接上下文
- 把本轮的思考 + 工具结果追加到上下文里,给下一轮模型使用
- 模型根据上下文决策
- 读取历史信息,继续生成新指令,或判断任务完成
- 设置终止条件
- 当模型输出空工具(action 为空)时,结束循环,输出最终回答
流程图
8.4 核心代码
fromparserimportparserfromtoolsimportTOOL_MAPdefinvoke(input):print(input)# 编写模拟模型逻辑,依据问题内容,产出规范格式的思考、动作、参数文本ifinput.find("计算")!=-1:test_input="""Thought: 1+1结果是什么 Action: calculator Action Input: 1+1"""elifinput.find("查询")!=-1:test_input="""Thought: 查询项目相关说明 Action: doc_search Action Input: 项目介绍"""elifinput.find("时间")!=-1:test_input="""Thought: 现在是什么时间 Action: time_query Action Input: """else:test_input="""Thought: 问题已解决 Action: Action Input:"""returntest_inputif__name__=='__main__':# 对话上下文context={"user_query":"","history":[]}# 思考:义接收用户问题的入口llm_output=invoke("现在是什么时间")context['user_query']='现在是什么时间'whileTrue:# 调用解析器,拆分出思考内容、工具标识、传入参数input_parser=parser(llm_output)# 当 action 为空时退出循环ifinput_parser['action']=='':breakres=''# 检索工具映射表,校验当前工具是否合法可用ifinput_parser['action']notinTOOL_MAP:print('当前工具不可用')break# 跳出循环,结束Agentelse:# 行动:匹配到对应工具函数,传入参数执行运算查询tool=TOOL_MAP[input_parser['action']]# 汇总思考过程与工具执行结果,统一对外输出output=tool(input_parser['action_input'])res+=f"thought:{input_parser['thought']}\n output:{output}"context['history'].append(res)print('---')print(context)# 复盘# 拼接上下文:用户问题 + 历史结果context_str=f"用户问题:{context['user_query']},历史结果:{str(context['history'])}"llm_output=invoke(context_str)运行结果
(局部)
9 全场景测试 + 异常兼容优化
9.1 测试场景清单
- 常规计算:帮我计算 1+1
- 资料查询:帮我查询项目介绍
- 时间查询:现在是什么时间
- 无需工具闲聊:你好呀
- 无效工具请求:帮我查询天气
9.2 代码优化要点
- 问题抽取成变量,方便切换测试
- 增加异常捕获,规避执行报错
- 输出规整最终总结答复
9.3 验证标准
- 合法请求正常调用工具、循环迭代后自动收尾
- 无效工具、程序报错均可稳妥终止,不会死循环
- 各类意图场景都能按逻辑分支正确响应
项目6 Function Call 智能工具助手
项目核心功能
- 实现 3 个工具
- 查天气工具
- 查当前时间工具
- 简单知识库检索工具
- 自动调用工具
- 用户问问题 → 模型自己判断要不要调用工具、调用哪个、传什么参数
- 自定义解析器
- 自己写解析函数,控制 Agent 什么时候停止思考、停止调用工具
- 最后必须输出结构化 JSON
- 不管中间调用多少次工具,最终回答强制输出标准 JSON,不能随便说话
Function Call
核心本质就是大模型主动调用外部工具。
核心逻辑
- 模型本身知识有边界、没法实时数据、没法运算检索,就靠 Function Call 向外求助
- 模型不再只输出文本,而是生成规范的工具调用指令,包含工具名、入参
- 程序解析指令,执行对应工具拿到结果,再回传给模型
模型结合工具返回内容,继续推理或是给出最终答复 - 项目里的查天气、时间、知识库检索,全都是靠这套机制实现调度
开发步骤
- 阶段 1:环境准备 + 基础配置
- 安装必要依赖(langchain、langchain-openai 等)
- 配置大模型(GPT / 通义千问 / 文心一言 都行)
- 阶段 2:编写 3 个自定义工具
- 写一个 “查天气” 工具(简单模拟返回)
- 写一个 “查当前时间” 工具
- 写一个 “知识库检索” 工具(模拟本地知识)
- 阶段 3:学习并使用 bind_tools
- 把工具绑定给大模型
- 理解 Function Call 原理:模型什么时候决定调用工具
- 阶段 4:搭建 AgentExecutor 执行器
- 搭建 Agent 核心运行流程
- 理解:用户提问 → 思考 → 调用工具 → 获取结果 → 再思考
- 阶段 5:编写自定义解析器
- 自己写 parse 函数
- 控制:什么时候停止调用工具、进入最终回答
- 阶段 6:强制输出 JSON 格式(Response 伪装)
- 让模型最后必须调用 Response 工具
- 输出固定结构 JSON,而不是自然语言
- 阶段 7:完整测试
- 测试各种问题:
- 只需要查时间
- 需要查天气
- 需要查知识库
- 混合问题
- 看 Agent 能不能自动调度工具 + 最后输出 JSON
- 测试各种问题:
1 环境准备 & 基础配置
- 先引入环境变量、模型、工具相关基础模块
- 读取密钥接口地址,实例化大模型,温度设 0 保证推理稳定
2 自定义工具
LangChain 自定义工具的固定规则
- 用 @tool 装饰器修饰一个函数
- 函数名 = 工具名
- 函数文档字符串(注释)= 给大模型看的工具说明(非常重要)
- 函数参数 = 工具需要的入参
- 函数返回值 = 工具执行结果
核心代码
fromdatetimeimportdatetimefromlangchain_core.toolsimporttool@tooldefget_current_time():returndatetime.now().strftime("%Y-%m-%d %H:%M:%S")@tooldefget_weather(city):returnf"{city}市 晴天,25℃"documents={"项目介绍":"这是一个无FunctionCall的结构化Agent","工具列表":"计算器、时间查询、文档检索","使用规则":"必须按指定格式输出调用指令"}@tooldefknowledge_base_search(query):ifqueryindocuments:returndocuments[query]else:return"未找到相关文档"3 bind_tools
什么是 bind_tools
- 将自定义的工具 “告诉” 大模型让模型知道:这里有几个工具可以用
需要做的事情
- 把你定义的 3 个工具放进一个列表
- 用模型的 .bind_tools() 方法,把这个列表传进去
- 得到一个绑定了工具的新模型对象
规则
- 工具列表 = [工具 1, 工具 2, 工具 3]
- 直接调用 chat_model.bind_tools(工具列表)
- 赋值给一个新变量,比如 llm_with_tools
核心代码
tools=[get_current_time,get_weather,knowledge_base_search]chat_model_with_tools=chat_model.bind_tools(tools)4 AgentExecutor 执行器
必须先创建一个 Agent:
create_tool_calling_agent- 绑定好工具的模型(chat_model_with_tools)
- 工具列表(tools)
- 一个 prompt 提示词模板(系统提示 + 用户输入)
创建 AgentExecutor —— Agent 的 “运行器”
- 第一步创建的 agent
- 工具列表tools
- 可以加:verbose=True 看思考过程(强烈建议开)
executor.invoke ()
- 传入用户问题,即可自动调用工具
核心代码
fromlangchain_classic.agentsimportcreate_tool_calling_agent,AgentExecutorfromlangchain_core.promptsimportChatPromptTemplate,MessagesPlaceholder chat_prompt_template=ChatPromptTemplate.from_messages([("system","你是工具助手,必须调用工具回答"),("user","{user_input}"),MessagesPlaceholder("agent_scratchpad")])agent=create_tool_calling_agent(chat_model_with_tools,tools,chat_prompt_template)executor=AgentExecutor(agent=agent,tools=tools,verbose=True)res=executor.invoke({"user_input":"南京天气如何"})print(res)运行结果
5 自定义解析器
脱离默认 AgentExecutor 黑盒,手动解析工具调用、自主控制流程启停。
执行流程
- 步骤 1:梳理核心依赖与前置准备
- 现有资源:已绑定工具的模型、工具列表、提示词模板,直接复用。
- 核心对象:模型返回结果里的 tool_calls 字段,用来判断是否要调用工具。
- 步骤 2:实现工具匹配执行逻辑
- 定义一个独立函数,入参设计为工具名、工具参数。
- 遍历全局工具列表,根据工具名匹配对应工具。
- 匹配成功就执行工具并返回结果;匹配失败返回提示文本。
- 步骤 3:构造首轮对话上下文
- 用提示词模板组装请求内容,传入用户问题。
- agent_scratchpad 首轮传空列表,用来存放后续交互记录。
- 步骤 4:自定义解析 & 流程控制(核心)
- 调用模型得到响应结果。
- 判断响应是否存在 tool_calls:
- 存在:提取工具名、入参,调用上面的工具执行函数拿到结果,手动终止本轮流程。
- 不存在:直接取模型文本内容作为最终回答,流程结束。
自定义解析器的自定义体现在:自己判断工具、自己拆字段、自己找工具、自己控制终止
核心代码
defexecute_action(res):fortoolintools:iftool.name==res.tool_calls[0]['name']:res=tool.invoke(res.tool_calls[0]['args'])returnresreturn'未找到对应的工具,请重试'chain=chat_prompt_template|chat_model_with_tools res=chain.invoke({"user_input":"我在胡言乱语啊啊啊啊啊","agent_scratchpad":[]})print(res)ifres.tool_calls:res1=execute_action(res)print(res1)else:# 直接提取文本内容,作为最终回复,流程结束。print(res.content)运行结果
正常情况
南京市 晴天,25℃异常情况
content='哈哈,听起来你是在释放压力或者玩文字游戏呢!如果你有任何问题、需要帮助,或者只是想聊点什么,我都很乐意陪你~ 😄 \n要不要试试问点有趣的、实用的,或者来点脑洞大开的问题?'additional_kwargs={}response_metadata={'finish_reason':'stop','request_id':'c75a6e27-08dc-9e7b-8461-c769b09fd5b6','token_usage':{'input_tokens':258,'output_tokens':53,'total_tokens':311,'prompt_tokens_details':{'cached_tokens':0}}}id='lc_run--019e64d3-f637-7820-b912-8f95b0aeb04f'tool_calls=[]invalid_tool_calls=[]哈哈,听起来你是在释放压力或者玩文字游戏呢!如果你有任何问题、需要帮助,或者只是想聊点什么,我都很乐意陪你~ 😄 要不要试试问点有趣的、实用的,或者来点脑洞大开的问题?6 强制结构化输出
本节主要内容为:约束模型输出格式 + 自定义解析 JSON 内容。
提示词修改为
chat_prompt_template=ChatPromptTemplate.from_messages([("system","你是工具助手,必须调用工具回答。只输出 JSON,不要说任何多余的话!。如果是纯文本信息,那么回复的内容有type(值为text)和content(值为回答的内容)\n""属性,如果是工具调用相关,那么是type(值为tool)和tool_name(要调用的工具名称)和 args(工具函数的入参)"),("user","{user_input}"),MessagesPlaceholder("agent_scratchpad")])chat_model不再需要绑定tools。在上述提示词模板下,输出如下。
content='{"type": "tool", "tool_name": "get_weather", "args": {"city": "南京"}}'additional_kwargs={}response_metadata={'finish_reason':'stop','request_id':'7838f54d-ec09-9226-922e-961fc962beb1','token_usage':{'input_tokens':96,'output_tokens':23,'total_tokens':119,'prompt_tokens_details':{'cached_tokens':0}}}id='lc_run--019e64e6-1126-7210-8933-0a5d91a01577'tool_calls=[]invalid_tool_calls=[]对输出进行解析,修改解析器
defexecute_action(res):fortoolintools:iftool.name==res['tool_name']:res=tool.invoke(res['args'])returnresreturn'未找到对应的工具,请重试'chain=chat_prompt_template|chat_model res=chain.invoke({"user_input":"南京天气如何","agent_scratchpad":[]})print(res)content=json.loads(res.content)ifcontent['type']=='tool':res1=execute_action(content)print(res1)else:# 直接提取文本内容,作为最终回复,流程结束。print(content)运行结果
南京市 晴天,25℃本节目的
- 在不使用模型原生工具调用(不 bind_tools)的前提下
- 通过提示词强制模型输出【自定义结构化 JSON】
- 手动解析格式、手动匹配工具、手动执行调用,最终拿到结果
