AI智能体对话平台开发实战(二):地基搭建——从零开始写第一行代码
📖 引言
在上一篇中,我们用七分时间完成了思考:分析了需求、对比了方案、选定了技术栈、画出了架构图。就像盖房子之前,我们已经画好了图纸,选好了材料。
现在,是时候拿起砖块,开始真正的搭建了。
这一篇,我们将从零开始,把上一篇的设计思路变成实实在在的代码。当你完成这一篇的学习,你将拥有一个可以运行的Express服务器,能够通过API动态发现智能体,并能在浏览器中看到第一个版本的智能体列表。
这将是MultiMind的第一块里程碑。

🎯 本章目标
学完本篇,你将能够:
- 初始化Node.js项目并安装必要依赖
- 搭建Express服务器基础框架
- 实现智能体动态扫描的核心逻辑
- 创建第一个智能体(助手小M)
- 通过浏览器或curl验证API接口
- 理解路由、中间件、文件操作的核心概念
📦 前置准备
在开始编码前,请确保你的开发环境已经准备好:
| 工具 | 版本要求 | 验证命令 |
|---|---|---|
| Node.js | 14.x 或更高 | node -v |
| npm | 6.x 或更高 | npm -v |
| 编辑器 | 任意 | VSCode推荐 |
如果你还没有安装Node.js,请前往官网下载安装。安装完成后,在终端运行验证命令,看到版本号即表示成功。
🚀 第一步:项目初始化
创建项目目录
打开终端,执行以下命令:
# 创建项目目录
mkdir ai-chat-agents
cd ai-chat-agents
# 初始化package.json
npm init -y
npm init -y会生成一个默认的package.json文件。这个文件是Node.js项目的“身份证”,记录了项目的名称、版本、依赖等信息。
根据我们的需求,修改package.json:
{
"name": "ai-chat-agents",
"version": "1.0.0",
"description": "AI智能体对话平台",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": ["ai", "chatbot", "agents"],
"author": "你的名字",
"license": "MIT"
}
设计思考:为什么把main改成server.js?因为我们的后端入口文件叫server.js更直观。为什么加dev脚本?nodemon可以在代码修改后自动重启服务器,开发时非常方便。为什么用MIT许可证?MIT是最宽松的开源许可证,允许别人自由使用和修改。
安装依赖
接下来安装项目依赖:
# 安装生产依赖
npm install express
# 安装开发依赖
npm install --save-dev nodemon
依赖说明:
- express:Node.js Web框架,用于创建API服务。它简化了路由、中间件、请求处理等操作,是Node.js后端开发的事实标准。
- nodemon:开发工具,监听文件变化自动重启服务器,让我们在修改代码后不用手动重启,极大提升开发效率。
安装完成后,你会发现项目里多了两个文件:
node_modules/:存放所有安装的包(很大,不要动它)package-lock.json:锁定依赖版本,确保团队开发时依赖一致
🏗️ 第二步:创建项目骨架
创建目录结构
按照上一篇的架构设计,我们需要创建以下目录和文件:
# 创建public目录(存放前端静态文件)
mkdir public
# 创建第一个智能体目录
mkdir "助手小M"
# 创建智能体文件(先创建空文件,内容稍后填写)
touch "助手小M/助手小M.txt"
touch "助手小M/助手小M.png" # 实际使用时,你需要放一张真实的图片
现在项目结构变成:
ai-chat-agents/
├── node_modules/
├── public/ # 前端静态文件
├── 助手小M/ # 第一个智能体
│ ├── 助手小M.txt
│ └── 助手小M.png
├── package.json
└── package-lock.json

编写第一个智能体的prompt
编辑助手小M/助手小M.txt,写入以下内容:
你是助手小M,一个友好热情的AI助手。
性格特点:
- 温和有礼,总是用礼貌的语气说话
- 乐于助人,对每个问题都认真回答
- 略带幽默感,但不失专业
对话规则:
1. 初次见面要自我介绍
2. 回答要简洁明了,不说废话
3. 遇到不懂的问题要坦诚承认
设计思考:这个prompt虽然简单,但已经包含了人格设定的三个核心要素:身份定义、性格描述、行为规则。后续我们会看到,正是这些设定让AI的回复有了“人格”。
关于头像文件的说明:目前我们只是创建了一个空的.png文件。在实际使用中,你需要准备一张真正的图片。如果你暂时没有合适的图片,可以先用一张占位图。注意:文件名必须完全匹配——文件夹叫“助手小M”,文件就必须叫“助手小M.png”。这是我们的约定规则。
⚙️ 第三步:编写服务器基础代码
创建server.js
在项目根目录创建server.js,写入最基础的Express服务器代码:
// 引入依赖
const express = require('express');
const fs = require('fs');
const path = require('path');
// 创建Express应用
const app = express();
// 定义端口
const PORT = 3002;
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
});
代码解析:
require('express'):引入Express模块require('fs'):引入文件系统模块(内置,无需安装)require('path'):引入路径处理模块(内置)express():创建Express应用实例app.listen(PORT, callback):启动服务器,监听指定端口
现在运行试试:
node server.js
你应该看到输出:🚀 服务器运行在 http://localhost:3002
按 Ctrl+C 可以停止服务器。
添加中间件
Express通过中间件来处理请求。我们需要添加两个基础的中间件:
// 中间件配置
app.use(express.json()); // 解析JSON格式的请求体
app.use(express.static('public')); // 提供静态文件服务
中间件说明:
express.json():当客户端发送JSON数据时,自动解析并挂在req.body上express.static('public'):将public目录暴露为静态资源,如http://localhost:3002/index.html
添加静态路由
为了让前端能访问智能体的头像图片,还需要添加一个静态路由:
// 提供智能体文件夹的静态访问
app.use('/agents', express.static('.'));
这行代码的意思是:当用户访问/agents/xxx时,去项目根目录(.)查找对应的文件。例如,访问http://localhost:3002/agents/助手小M/助手小M.png,就会返回./助手小M/助手小M.png文件。
安全思考:这样会不会暴露整个项目目录?是的,确实会。但我们的项目结构简单,目前只有智能体文件夹需要对外访问。如果担心安全问题,可以只暴露特定的目录。
🔍 第四步:实现智能体发现接口
现在来实现最核心的功能——智能体动态发现。
理解需求
我们需要一个API接口:GET /api/agents,返回所有智能体的列表。返回的数据格式应该是:
{
"success": true,
"agents": [
{
"name": "助手小M",
"prompt": "你是助手小M...",
"image": "/agents/助手小M/助手小M.png"
}
]
}
实现思路
要实现这个接口,我们需要:
- 扫描项目根目录,找出所有文件夹
- 对每个文件夹,检查是否包含
文件夹名.png和文件夹名.txt - 如果都包含,读取
.txt文件内容作为prompt - 组装成JSON返回
核心代码片段
// 获取所有智能体列表
app.get('/api/agents', (req, res) => {
try {
const agents = [];
const directory = path.join(__dirname);
// 读取当前目录,获取所有文件夹
const folders = fs.readdirSync(directory, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
folders.forEach(folder => {
const folderPath = path.join(directory, folder);
const files = fs.readdirSync(folderPath);
// 检查是否存在对应名称的.png和.txt文件
const hasPng = files.includes(`${folder}.png`);
const hasTxt = files.includes(`${folder}.txt`);
if (hasPng && hasTxt) {
// 读取prompt内容
const promptPath = path.join(folderPath, `${folder}.txt`);
const prompt = fs.readFileSync(promptPath, 'utf8').trim();
agents.push({
name: folder,
prompt: prompt,
image: `/agents/${folder}/${folder}.png`
});
}
});
res.json({ success: true, agents });
} catch (error) {
console.error('获取智能体列表失败:', error);
res.status(500).json({ success: false, error: '获取智能体列表失败' });
}
});
关键点思考
1. 为什么用fs.readdirSync而不是异步方法?
在Node.js中,异步是主流,但这里我刻意用了同步方法:
| 同步 | 异步 |
|---|---|
| 代码简单,线性执行 | 需要回调或Promise,代码复杂 |
| 会阻塞事件循环 | 不会阻塞 |
| 适合启动时执行 | 适合高并发场景 |
选择原因:智能体列表只在启动和手动刷新时读取,数据量小(几十个文件夹),代码简洁易懂更重要。如果未来智能体数量达到上千个,或者API被频繁调用,可以优化为异步。
2. { withFileTypes: true }的作用是什么?
不加这个参数,fs.readdirSync返回的是文件名数组。加上后,返回的是包含文件类型信息的对象数组,我们可以直接用.isDirectory()判断是不是文件夹,而不需要再次调用fs.statSync()。这是一个性能优化的小技巧。
3. 为什么用path.join处理路径?
__dirname是Node.js的全局变量,表示当前文件所在的目录绝对路径。path.join会根据操作系统自动使用正确的路径分隔符(Windows用\,Linux/mac用/),避免手动拼接带来的跨平台问题。
4. try-catch 错误处理的重要性
任何一步都可能出错(比如文件夹被占用、文件权限不够),用try-catch捕获所有错误,返回统一的错误格式。这样客户端就能知道是服务器出错,而不是自己的问题。
接口测试
现在我们可以测试这个接口了。确保服务器在运行:
npm run dev
打开浏览器,访问:http://localhost:3002/api/agents
你应该看到类似这样的JSON返回:
{
"success": true,
"agents": [
{
"name": "助手小M",
"prompt": "你是助手小M,一个友好热情的AI助手。\n\n性格特点:\n- 温和有礼...",
"image": "/agents/助手小M/助手小M.png"
}
]
}
🎉 恭喜!你的第一个API接口跑通了!
🖥️ 第五步:创建前端页面(简略版)
有了API接口,我们还需要一个前端页面来展示这些智能体。这里只给出核心思路和代码片段,完整的样式代码可以参考项目源码。
核心思路
- 创建
public/index.html作为入口 - 页面加载时调用
/api/agents获取数据 - 动态渲染智能体卡片
- 处理加载状态和错误情况
核心代码片段
<div id="agentsContainer">
<div class="loading">加载中...</div>
</div>
<script>
async function loadAgents() {
try {
const response = await fetch('/api/agents');
const data = await response.json();
const container = document.getElementById('agentsContainer');
if (data.success && data.agents.length > 0) {
container.innerHTML = data.agents.map(agent => `
<div class="agent-card">
<img src="${agent.image}" alt="${agent.name}">
<h3>${agent.name}</h3>
<p>${agent.prompt.substring(0, 100)}...</p>
</div>
`).join('');
} else {
container.innerHTML = '<div>暂无智能体,请先创建</div>';
}
} catch (error) {
container.innerHTML = '<div>加载失败,请刷新重试</div>';
}
}
document.addEventListener('DOMContentLoaded', loadAgents);
</script>
设计思考:
- 为什么用
substring(0, 100)?因为prompt可能很长,在卡片上只显示前100个字符作为预览,保持界面整洁。 - 为什么用
onerror处理图片?网络图片可能加载失败,提供一个后备方案提升用户体验。
🧪 第六步:测试与验证
创建更多智能体来测试
为了验证动态发现机制,我们再创建几个智能体:
# 创建第二个智能体
mkdir "鲁迅"
touch "鲁迅/鲁迅.txt"
touch "鲁迅/鲁迅.png"
# 创建第三个智能体
mkdir "凌云"
touch "凌云/凌云.txt"
touch "凌云/凌云.png"
给它们写入不同的prompt:
鲁迅/鲁迅.txt
你是鲁迅,中国现代文学的奠基人。
- 冷峻、犀利、幽默
- 常以笔为刀,解剖社会
- 说话带着绍兴口音,爱用比喻
凌云/凌云.txt
你是凌云,隐于市的程序员。
- 儒雅,书卷气,言语间带古风
- 被问技术问题,只谈思路不谈实现
- 若被追问代码,便说“不想写”
刷新页面,你会看到新智能体自动出现在列表中——不需要重启服务器,不需要改任何配置。
这就是“文件夹即智能体”的魔力!
用curl测试API
如果你更喜欢命令行,也可以用curl测试:
curl http://localhost:3002/api/agents
会返回格式化的JSON数据。
🔧 第七步:扩展思考
思考1:如何支持更多图片格式?
目前只支持.png。如果要支持.jpg、.jpeg、.gif,可以这样改:
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif'];
const hasImage = imageExtensions.some(ext =>
files.includes(`${folder}${ext}`)
);
找到图片后,需要记录实际的文件名,而不是写死.png。这引出一个问题:如果同时存在.png和.jpg,应该用哪个? 可以定义优先级,比如优先使用png,或者让用户指定。
思考2:如何添加缓存?
每次请求都扫描磁盘,如果有性能问题,可以添加内存缓存:
let agentsCache = null;
let cacheTime = 0;
const CACHE_TTL = 60000; // 1分钟
app.get('/api/agents', (req, res) => {
// 如果缓存存在且未过期,直接返回缓存
if (agentsCache && Date.now() - cacheTime < CACHE_TTL) {
return res.json(agentsCache);
}
// ...扫描逻辑...
// 更新缓存
agentsCache = { success: true, agents };
cacheTime = Date.now();
res.json(agentsCache);
});
但这也带来了新问题:如果用户在缓存期间新增了智能体,列表不会立即更新。怎么解决?可以提供一个手动刷新的接口,或者在修改文件时主动清理缓存。
思考3:如何处理文件名大小写?
Windows不区分大小写,Linux区分。这是跨平台开发中常见的坑。比如用户在Windows上创建了“鲁迅”文件夹,里面放的是“LuXun.png”,在Linux上就识别不出来。
解决方案:在扫描时做大小写不敏感匹配:
const filesLower = files.map(f => f.toLowerCase());
const hasPng = filesLower.includes(`${folder.toLowerCase()}.png`);
但这样做有个问题:返回的图片路径需要是实际的文件名,而不是小写后的。所以还需要找到原始文件名:
const actualPngFile = files.find(f =>
f.toLowerCase() === `${folder.toLowerCase()}.png`
);
思考4:如何保证智能体数据的完整性?
如果用户只放了.png文件,忘了放.txt,这个智能体算不算有效?
几种方案:
- 严格模式:必须两个文件都存在,才算有效
- 宽松模式:只要有头像就算,prompt用默认值
- 智能模式:缺什么补什么,比如没有txt就自动生成一个
初期我们选择严格模式,因为逻辑简单,符合预期。但可以在UI上提示“不完整智能体”,引导用户补全。
思考5:文件夹名包含特殊字符怎么办?
如果文件夹名包含空格、中文标点等,URL编码会带来问题。比如/agents/助手小M/助手小M.png在浏览器中会自动编码,通常没问题。但如果文件夹名是测试/智能体(包含斜杠),就会破坏路径结构。
最佳实践:建议文件夹名只使用字母、数字、中文和下划线,避免使用特殊字符。
🎯 本篇小结
在这一篇中,我们真正动手写代码,完成了:
| 任务 | 成果 |
|---|---|
| 项目初始化 | 创建了package.json,安装了express和nodemon |
| 服务器搭建 | 创建了基础Express服务器,配置了中间件 |
| 智能体创建 | 创建了“助手小M”等三个智能体 |
| API实现 | 实现了GET /api/agents动态发现接口 |
| 前端页面 | 创建了简单的智能体卡片列表 |
| 测试验证 | 验证了动态发现机制,测试了异常情况 |
核心收获:
- “文件夹即智能体”的设计理念已经通过代码实现
- 新增智能体 = 新建文件夹 + 放两张文件,无需改代码
- 所有智能体数据集中在各自的文件夹里,易于管理
- 前端自动渲染,无需手动配置
🔮 下篇预告
第三篇:对话之心——实现多轮对话上下文管理
在下一篇中,我们将实现真正的对话功能:
- 聊天界面的设计与实现
- 对话历史的数据结构设计
- 消息的发送与接收
- 对话上下文的维护
- 第一版AI对话集成
敬请期待!
📝 写在最后
从想法到代码,从架构到实现,MultiMind已经迈出了坚实的第一步。
现在的系统虽然简单,但已经具备了最核心的能力:动态发现智能体。这为后续所有功能打下了基础。
回顾这一篇,你会发现我们并没有写很多代码——核心的API接口只有几十行。但每一行代码背后,都有上一篇的深思熟虑作为支撑。这就是“七分思考,三分编码”的意义。
代码是思维的映射。好的代码,源于好的思考。
下一篇见!
思考题:现在的智能体发现机制要求文件夹名必须和文件名完全一致。你觉得这个设计合理吗?如果遇到文件夹名包含特殊字符(如空格、中文标点),会有什么问题?欢迎在评论区分享你的思考。
更多推荐




所有评论(0)