我的第一个工具调用Agent:从零构建“知识库+计算器+时间+文件”四合一智能体

📅 2026/7/12 23:58:50 👁️ 阅读次数 📝 编程学习
我的第一个工具调用Agent:从零构建“知识库+计算器+时间+文件”四合一智能体

从RAG到Agent,我用一个周末实现了AI从"知道"到"做到"的跃迁

一、引言:为什么从RAG升级到Agent?

作为制造业数字化转型从业者,2026年上半年我完成了几个本地化AI应用实践——环境搭建、多模态图搜、RAG知识库问答,甚至还把橘子洲头做成了AI客服。这些应用让AI"知道"了很多事,但总感觉缺了点什么:AI只能被动回答问题,不能主动调用工具去解决问题

直到我开始接触AI Agent,才真正理解了这个核心差异:

  • RAG(检索增强生成):AI = 大脑(模型) + 资料库(知识库),能"知道"并"回答"

  • Agent(智能体):AI = 大脑(模型) + 资料库(知识库) +手脚(工具),能"做到"并"完成"

本案例的目标是构建一个具备知识库检索、数学计算、时间查询、文件读写四项能力的Agent,通过实践完整理解Agent的核心机制。这对于我们后续在制造企业落地数据质量巡检、主数据处理、数仓分层设计等场景具有直接的参考价值。

运行效果图:

二、技术方案与选型

我的开发环境比较特殊,可能也代表了一部分开发者的现状:Windows 11 + WSL2(Ubuntu 22.04)+ 32GB内存 + 无独显,大模型通过Ollama(安装在Windows侧)提供API服务,代码在WSL中运行。

组件选型说明
大模型Qwen2.5:7B(Ollama)中文友好,7B参数量在CPU上可运行
Agent框架LangChain 0.3.x社区成熟,ReAct模式原生支持
向量数据库Chroma轻量级,与之前RAG案例无缝衔接
嵌入模型all-MiniLM-L6-v2384维,CPU友好,中文效果尚可
Web框架Flask轻量级,快速构建交互界面

工程化原则(贯穿整个专栏):

  • 配置与代码分离(config.py

  • 工具函数独立封装(tools/模块)

  • 完整的错误处理与日志记录

  • 单元测试覆盖每个工具函数

  • 项目结构清晰,可一键运行

三、项目结构:工程化的起点

case1_tool_agent/ ├── config.py # 配置集中管理(路径、模型、端口) ├── requirements.txt # 依赖锁定版本 ├── .env.example # 环境变量模板 ├── tools/ # 工具模块(可复用) │ ├── __init__.py │ ├── calculator.py # 计算器工具 │ ├── time_tool.py # 时间工具 │ ├── file_tool.py # 文件读写工具 │ └── rag_retriever.py # 知识库检索工具 ├── agent/ # Agent核心 │ ├── __init__.py │ └── builder.py # Agent构建工厂 ├── web/ # Web服务 │ ├── app.py # Flask主应用 │ └── templates/ │ └── index.html # 极简Chat界面 ├── scripts/ # 辅助脚本 │ ├── test_tools.py # 工具单元测试 │ └── test_cli.py # 命令行交互测试 ├── chroma_db/ # 知识库(复用已有) │ └── chroma.sqlite3 └── workspace/ # 文件工具工作目录

📌工程心得:我一直信奉"好的项目结构是成功的一半"。清晰的模块划分让工具可以在后续案例中直接复用,配置集中管理避免硬编码,单元测试保障每次修改不引入新Bug。

四、核心实现:四个工具的定义与编码

4.1 计算器工具(tools/calculator.py

# -*- coding: utf-8 -*- # @author: javy21 # @blog: https://blog.csdn.net/javy21 """ 计算器工具:执行数学表达式计算 """ import logging from langchain.tools import tool logger = logging.getLogger(__name__) @tool def calculate(expression: str) -> str: """ 执行数学计算,输入数学表达式,返回计算结果。 Args: expression: 数学表达式,如 '3+5' 或 '10*2/3' Returns: 计算结果字符串,或错误信息 """ # 安全检查:只允许数字、运算符、括号、空格 allowed_chars = set("0123456789+-*/().% ") if not all(c in allowed_chars for c in expression): error_msg = f"表达式包含非法字符:{expression}" logger.warning(error_msg) return f"计算错误:{error_msg}" try: result = eval(expression) logger.info(f"计算成功: {expression} = {result}") return f"计算结果:{result}" except ZeroDivisionError: return "计算错误:除数不能为零" except Exception as e: return f"计算错误:表达式格式错误 - {str(e)}"

📌设计要点

  • 使用@tool装饰器将函数转换为LangChain工具

  • 输入校验防止代码注入(生产环境建议用numexpr

  • 完整的异常处理返回友好信息而非堆栈

4.2 时间工具(tools/time_tool.py

# -*- coding: utf-8 -*- # @author: javy21 # @blog: https://blog.csdn.net/javy21 """ 时间工具:获取当前日期和时间 """ import logging from datetime import datetime from langchain.tools import tool logger = logging.getLogger(__name__) @tool def get_current_time(format: str = "%Y-%m-%d %H:%M:%S") -> str: """ 获取当前日期和时间,可指定输出格式。 Args: format: 时间格式字符串,默认 '%Y-%m-%d %H:%M:%S' Returns: 格式化后的当前时间字符串 """ now = datetime.now() result = now.strftime(format) logger.info(f"获取当前时间: {result}") return result

4.3 文件读写工具(tools/file_tool.py

# -*- coding: utf-8 -*- # @author: javy21 # @blog: https://blog.csdn.net/javy21 """ 文件读写工具:在workspace目录下读写文件(防路径穿越) """ import logging from pathlib import Path from langchain.tools import tool from config import WORKSPACE_DIR logger = logging.getLogger(__name__) @tool def read_file(filename: str) -> str: """读取指定文件的内容。""" # 安全检查:防止路径穿越攻击 if ".." in filename or filename.startswith("/"): return "读取失败:非法文件名" filepath = WORKSPACE_DIR / filename if not filepath.exists(): return f"读取失败:文件 '{filename}' 不存在" try: content = filepath.read_text(encoding="utf-8") logger.info(f"读取文件成功: {filename}") return f"文件内容:\n{content}" except Exception as e: return f"读取失败:{str(e)}" @tool def write_file(filename: str, content: str) -> str: """将内容写入指定文件。""" if ".." in filename or filename.startswith("/"): return "写入失败:非法文件名" filepath = WORKSPACE_DIR / filename try: filepath.write_text(content, encoding="utf-8") logger.info(f"写入文件成功: {filename}") return f"成功写入文件:{filename}" except Exception as e: return f"写入失败:{str(e)}"

⚠️安全提醒:文件工具是企业场景中最容易出安全问题的环节,一定要做好路径穿越防护。

4.4 知识库检索工具(tools/rag_retriever.py

# -*- coding: utf-8 -*- # @author: javy21 # @blog: https://blog.csdn.net/javy21 """ 知识库检索工具:基于Chroma向量库的语义检索 """ import logging from langchain.tools import tool from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from config import CHROMA_DIR, EMBEDDING_MODEL, RETRIEVAL_TOP_K logger = logging.getLogger(__name__) _vectordb = None def _get_vectordb(): """懒加载向量数据库(单例模式)""" global _vectordb if _vectordb is None: logger.info(f"正在加载向量数据库: {CHROMA_DIR}") embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL) _vectordb = Chroma( persist_directory=str(CHROMA_DIR), embedding_function=embeddings, ) logger.info("向量数据库加载完成") return _vectordb @tool def search_knowledge(query: str) -> str: """从知识库中检索与查询相关的文档片段。""" try: vectordb = _get_vectordb() docs = vectordb.similarity_search(query, k=RETRIEVAL_TOP_K) if not docs: return "未找到相关信息。" contents = [doc.page_content for doc in docs] result = "\n\n---\n\n".join(contents) logger.info(f"检索成功: '{query}' -> {len(docs)} 个片段") return f"检索结果:\n{result}" except Exception as e: return f"检索失败:{str(e)}"

五、Agent构建:让AI学会使用工具

Agent构建是本案例的核心环节,代码位于agent/builder.py

# -*- coding: utf-8 -*- # @author: javy21 # @blog: https://blog.csdn.net/javy21 """ Agent构建器:创建和配置LangChain Agent """ import logging from langchain_community.llms import Ollama from langchain.agents import initialize_agent, AgentType from langchain.memory import ConversationBufferMemory from config import OLLAMA_BASE_URL, MODEL_NAME, TEMPERATURE from tools import calculate, get_current_time, read_file, write_file, search_knowledge logger = logging.getLogger(__name__) def create_agent(verbose: bool = True): """创建并返回一个配置好的Agent实例""" # 1. 初始化大模型 logger.info(f"初始化模型: {MODEL_NAME} @ {OLLAMA_BASE_URL}") llm = Ollama( model=MODEL_NAME, base_url=OLLAMA_BASE_URL, temperature=TEMPERATURE, ) # 2. 注册工具(顺序影响决策优先级) tools = [ search_knowledge, # 知识库检索 calculate, # 计算器 get_current_time, # 时间查询 read_file, # 文件读 write_file, # 文件写 ] # 3. 设置记忆(对话缓冲) memory = ConversationBufferMemory( memory_key="chat_history", return_messages=True, ) # 4. 创建Agent(使用支持多参数工具的Agent类型) agent = initialize_agent( tools, llm, agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, memory=memory, verbose=verbose, handle_parsing_errors=True, max_iterations=10, early_stopping_method="generate", ) logger.info("Agent创建成功") return agent

📌关键决策:我选择了STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION而非CONVERSATIONAL_REACT_DESCRIPTION,因为前者支持多参数工具(如write_file需要 filename + content 两个参数),后者只支持单参数工具。

六、Web交互界面:极简Chat风格

Flask应用提供简洁的聊天界面,用户在浏览器中输入问题,Agent自动决策并调用相应工具:

核心路由逻辑(web/app.py):

@app.route("/api/chat", methods=["POST"]) def chat(): data = request.get_json() user_input = data.get("message", "").strip() if not user_input: return jsonify({"error": "消息不能为空"}), 400 try: response = agent.run(user_input) return jsonify({"response": response}) except Exception as e: logger.error(f"处理失败:{str(e)}") return jsonify({"error": f"处理失败:{str(e)}"}), 500

七、运行效果展示

7.1 单元测试通过

$ python scripts/test_tools.py ======================================== 开始工具单元测试 ======================================== 🧮 测试计算器... ✅ 计算器测试通过 🕐 测试时间工具... ✅ 当前时间: 2026-07-05 22:32:26 📁 测试文件工具... ✅ 文件工具测试通过 📚 测试知识库检索... ✅ 检索结果长度: 1024 字符 ======================================== ✅ 所有测试通过! ========================================

7.2 Web交互实测

用户输入Agent行为结果
"现在几点了?"调用时间工具返回当前时间
"帮我算一下 123 * 456"调用计算器返回 56088
"十五五规划中关于人工智能的内容"调用知识库检索返回相关文档片段
"把 'Hello Agent' 写入 test.txt"调用文件写工具返回成功信息
"读取 test.txt 的内容"调用文件读工具返回文件内容
"明天上午10点我要开会,帮我算一下还有多少秒"组合调用时间+计算器计算差值并返回

八、踩坑记录与解决方案

坑1:ModuleNotFoundError: No module named 'tools.calculator'

现象:运行测试脚本时,Python找不到自定义模块。

原因:直接运行python scripts/test_tools.py时,项目根目录不在sys.path中。

解决:在每个脚本开头添加路径处理:

python

复制

下载

import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent))

坑2:向量库为空,检索无结果

现象search_knowledge总是返回"未找到相关信息"。

原因:从之前项目复制的chroma_db只包含空表,没有实际向量数据。

解决:重新构建知识库,用PDF文档填充向量数据:

python

复制

下载

vectordb = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=str(CHROMA_DIR) )

坑3:ConversationalAgent does not support multi-input tool

现象:启动Web服务时报错,提示write_file是多参数工具不被支持。

原因ConversationalAgent要求所有工具都是单输入(只有一个参数)。

解决:更换为AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,该类型支持多参数工具。

坑4:Ollama连接失败(Connection refused)

现象:Agent调用大模型时,返回HTTPConnectionPool(host='localhost', port=11434)... Connection refused

原因:我的Ollama安装在Windows上,而代码在WSL中运行,WSL中的localhost指向WSL自身而非Windows宿主机。

解决

  1. 在Windows上配置Ollama监听所有接口:set OLLAMA_HOST=0.0.0.0:11434 && ollama serve

  2. 修改config.py中的OLLAMA_BASE_URL为Windows的IP地址

九、工程化改进方向(让Demo走向生产)

改进方向实现建议优先级
工具描述优化精炼每个工具的description,提高模型调用准确率P0
日志与监控集成结构化日志(如JSON格式),便于采集分析P0
超时与重试对Ollama调用增加超时和重试机制P1
环境变量管理使用.env+python-dotenv管理配置P1
容器化部署编写Dockerfile,实现一键部署P2

十、小结与下篇预告

通过本案例,我们完整实现了从0到1构建一个工具调用Agent的全过程,涵盖了工具定义、Agent构建、Web交互、单元测试、工程化组织等多个维度。在这个过程中,我深刻体会到:

Agent的核心不是"调用工具",而是"决定何时调用什么工具"。ReAct模式让AI具备了自主决策的能力,这是与传统自动化脚本的本质区别。

下篇预告

下一篇理论文章《从RAG到Agent:AI应用范式的演进——兼谈工具调用在制造业数据管理的落地价值》,我将从理论角度深入剖析:

  • RAG与Agent的本质区别与演进逻辑

  • ReAct模式的深层理解

  • 工具调用的关键设计原则

  • Agent在制造业数据管理(数据质量、数仓分层、数据标准)中的落地场景

敬请期待!欢迎在评论区交流您的实践心得或遇到的问题,一起探讨、共同成长。


作者:javy21
博客:javy21-CSDN博客
专栏:AI Agent从入门到落地
本文案例代码:可在评论区获取完整项目源码


📌如果您觉得本文有帮助,欢迎交流分享!您的支持是我持续输出的动力。