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

导航台账】老蒋的技术博客全系列文章汇总(持续更新)

本文收录于专栏 《从 RAG 到 Agent:20 年 IT 老兵的 AI 智能体实战笔记》,持续更新制造业本地化 AI 落地完整工程案例。

        作者是20 + 年 IT 老兵(10 年开发,5 + 年数据、团队、项目管理),深耕上市制造企业数据架构,全程本地开源大模型自研,聚焦制造业智能问答、数据质量巡检、数仓智能化等场景,分享可直接复用的工程化代码与踩坑经验。

从RAG到Agent,我用一个周末实现了AI从"知道"到"做到"的跃迁。下面是完整的过程笔记:

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

        作为制造业数字化转型从业者,2026年上半年我完成了几个本地化AI应用实践:

  1.  老攻城狮的AI开发环境搭建全记录:从零到跑通本地大模型(一日速通版)
  2. AI应用基础篇02-RAG知识库实战:从零搭建“十五五”规划智能问答系统
  3. 场景落地篇01-我把橘子洲头做成了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 在制造业数据管理(数据质量、数仓分层、数据标准)中的落地场景

💡 实用提示:本文为系列实战第 1 篇,后续将持续更新制造业场景的 Agent 落地案例,建议收藏 + 关注,避免错过完整系列工程源码。

📦 完整源码获取:对本项目完整工程源码感兴趣的朋友,可以在评论区留言「Agent 源码」,我会通过 CSDN 私信逐一发送完整项目压缩包。

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


作者:制造数据与 AI 践行者老蒋

专栏从 RAG 到 Agent:20 年 IT 老兵的 AI 智能体实战笔记 | 老攻城狮的 AI 编程实践之路

所有案例均为本地开源大模型环境自研,无云端 API 依赖


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


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值