本文收录于专栏 《从 RAG 到 Agent:20 年 IT 老兵的 AI 智能体实战笔记》,持续更新制造业本地化 AI 落地完整工程案例。
作者是20 + 年 IT 老兵(10 年开发,5 + 年数据、团队、项目管理),深耕上市制造企业数据架构,全程本地开源大模型自研,聚焦制造业智能问答、数据质量巡检、数仓智能化等场景,分享可直接复用的工程化代码与踩坑经验。
从RAG到Agent,我用一个周末实现了AI从"知道"到"做到"的跃迁。下面是完整的过程笔记:
一、引言:为什么从RAG升级到Agent?
作为制造业数字化转型从业者,2026年上半年我完成了几个本地化AI应用实践:
- 老攻城狮的AI开发环境搭建全记录:从零到跑通本地大模型(一日速通版)
- AI应用基础篇02-RAG知识库实战:从零搭建“十五五”规划智能问答系统
- 场景落地篇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-v2 | 384维,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宿主机。
解决:
-
在Windows上配置Ollama监听所有接口:
set OLLAMA_HOST=0.0.0.0:11434 && ollama serve -
修改
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从入门到落地
本文案例代码:可在评论区获取完整项目源码
📌 如果您觉得本文有帮助,欢迎交流分享!您的支持是我持续输出的动力。

658

被折叠的 条评论
为什么被折叠?



