1. AI代理人是什么:从通用对话到角色化定制
最近“AI代理人”这个词频繁出现在技术社区和产品发布会上。它和早期那种一问一答的聊天机器人有本质区别:传统聊天机器人只是被动地等你提问,AI代理人则更接近于一个具备自主对话风格、任务目标、记忆能力和行为边界的数字角色。简单说,AI代理人不再只是“回答问题的工具”,它更像一个“有性格、懂规矩、能连续协作”的智能实体。
本文要实践的“骨壳工坊 AI代理人 仕女型C1”就是一个典型的角色化AI代理人项目。和通用大模型对话不同,仕女型C1需要拥有特定人设:温婉、知性、含蓄,具备传统文化知识储备,说话方式带古典韵味,同时还得保证对话连续、记忆稳定、反馈可控。要让这些特质稳定成立,不能只靠模型“临场发挥”,而是要把角色设定、上下文管理、记忆策略、接口封装、内容安全全部工程化。
作为开发者,掌握这类项目至少有三个价值:第一,理解如何把大模型能力封装成可商用的角色化服务;第二,学会用Prompt工程、记忆模块和接口层构建一个完整的Agent系统;第三,掌握AI代理人项目的工程化落地方法和排查思路。下面我会从一个最小可运行的仕女型代理人项目入手,拆解从需求设计到代码实现再到部署验证的完整流程。本文不涉及训练大模型,也不需要昂贵的GPU,所有代码都基于现有LLM API完成,适合入门到进阶的开发者参考。
在正式编码之前,先强调一个基础概念:AI代理人 = 大模型引擎 + 人设约束 + 工作记忆 + 交互边界。大模型提供语言能力,人设约束决定说话内容和风格,工作记忆保证跨轮次一致性,交互边界则控制它能否调用外部工具或访问用户数据。仕女型C1目前的版本主要聚焦前三个要素,暂不引入复杂工具调用,这样能保证项目逻辑清晰,也方便后续扩展。
2. 仕女型C1需求分析与角色原型设计
动手写代码前,先把“仕女型C1”当成一个真实产品来分析。骨壳工坊推出这个项目时,核心目标是把中国传统仕女文化符号转化为可交互的数字化代理人。它不只是演示Demo,而是要能在文化场馆、线上导览、传统文化课程、轻陪伴场景中稳定使用。
2.1 目标用户与使用场景
仕女型C1的目标用户可以分为两类。一类是普通用户,他们希望通过与角色对话,感受传统文化氛围,了解诗词、书画、礼仪、节气知识;另一类是内容运营者,他们需要把角色嵌套进公众号、小程序或网页,作为特色互动模块使用。
典型场景有四种:
| 场景 | 用户诉求 | 技术侧重点 |
|---|---|---|
| 线上文化导览 | 边逛边听讲解 | 知识问答准确度 |
| 诗词书画教学 | 获取范例和典故 | 内容生成的规范性 |
| 情感陪伴 | 倾诉和排解 | 情感回应与边界控制 |
| 店铺/文旅引流 | 吸引关注、发放信息 | 稳定性和并发能力 |
本文的示例代码以“线上文化讲解+对话陪伴”为主,保证接口通用,你可以在其上加挂业务逻辑。
2.2 角色原型设定
仕女型C1的“性格内核”直接决定Prompt设计,所以先用产品语言把角色定义清楚:
- 角色姓名:玉簪。
- 身份背景:骨壳工坊虚拟人物,自幼研习书画,熟读典籍,擅长茶道与香道。
- 语言风格:温婉自然,善用文言点缀,不用生僻字堆砌,保持亲切感。
- 知识边界:只回答中国传统文化相关话题,涉及现代科技或时政问题礼貌回避。
- 情绪基调:平和、耐心,偶尔带一点俏皮,绝不自大、不冷漠。
- 记忆能力:记住用户名字、偏好,能回顾上一轮讨论内容。
为了保证角色输出稳定,这些设定最终会写入System Prompt,并辅以Few-shot示例固定输出风格。这一步非常重要,因为大模型默认输出偏“通用腔”,如果不做角色约束,它很快会背离人设。
2.3 功能模块拆分
仕女型C1需要以下功能模块:
- 对话管理:维护当前会话上下文,控制最长轮次。
- 记忆持久化:将用户偏好和关键信息写入SQLite。
- 角色Prompt装配:每次请求前动态生成完整System Prompt。
- 模型调用:调用OpenAI兼容接口,完成对话生成。
- API服务层:通过FastAPI提供HTTP接口。
- 前端交互:简单Web页面,用于演示。
模块拆分的好处是后续要替换模型、增加工具调用或扩展知识库,只需要修改对应模块,不会牵一发动全身。
3. 环境准备与项目结构
3.1 开发环境与依赖
为了兼顾灵活性和易用性,这个项目使用Python 3.10+开发,依赖以下核心库:
- openai:调用大模型API。
- fastapi:提供HTTP服务。
- uvicorn:ASGI服务器。
- pydantic:参数校验与数据模型。
- python-dotenv:读取环境变量。
- sqlite3:Python标准库,用于记忆持久化,无需额外安装。
具体版本不需要固定,以你本机安装时的最新稳定版本为准。这里的关键是你本机已经有Python环境和可用的OpenAI兼容API地址。如果你使用的是其他模型的API,只要它是OpenAI兼容格式,代码基本可以复用。
创建虚拟环境并安装依赖:
python3 -m venv venv source venv/bin/activate pip install openai fastapi uvicorn pydantic python-dotenv在项目根目录新建.env文件,注意不要把真实密钥提交到Git仓库:
OPENAI_API_KEY=你的密钥 OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_MODEL=gpt-4o-miniOPENAI_BASE_URL可以在不同模型服务商的兼容接口之间切换。如果本地有使用vLLM或Ollama启动的模型服务,也可以把地址指向本地。
3.2 项目结构说明
整个项目命名为bone_shell_workshop,结构如下:
bone_shell_workshop/ ├── agent/ │ ├── __init__.py │ ├── config.py # 全局配置 │ ├── prompt.py # 角色Prompt构建 │ ├── memory.py # 记忆管理 │ └── core.py # Agent核心逻辑 ├── app/ │ ├── __init__.py │ └── main.py # FastAPI接口 ├── frontend/ │ └── index.html # 简易演示页面 ├── data/ │ ├── .gitkeep │ └── memory.db # SQLite数据库(运行时生成) ├── .env.example ├── requirements.txt └── README.md我先在agent包里完成核心能力,再用app暴露接口,最后加一个简单前端页面。整个设计遵循分层思想,业务逻辑和HTTP层分离,后续替换框架相对容易。
4. 核心代码实现:Agent框架搭建
这是实战部分。我会按“配置 → Prompt → 记忆 → Agent → 接口”的顺序实现,每段代码会解释为什么这么写。
4.1 全局配置模块
文件路径:agent/config.py
import os from dotenv import load_dotenv load_dotenv() class Settings: """全局配置类,所有外部配置统一从这里读取。""" openai_api_key = os.getenv("OPENAI_API_KEY", "") openai_base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") openai_model = os.getenv("OPENAI_MODEL", "gpt-4o-mini") temperature = float(os.getenv("TEMPERATURE", "0.8")) max_tokens = int(os.getenv("MAX_TOKENS", "600")) memory_db_path = os.getenv("MEMORY_DB_PATH", "data/memory.db") max_history_rounds = int(os.getenv("MAX_HISTORY_ROUNDS", "10")) settings = Settings()读取密钥时没有在代码里硬编码,而是通过环境变量注入。这样既能避免密钥泄露,也方便在不同环境切换配置。max_history_rounds用来限制上下文轮数,防止请求超出模型上下文窗口。
4.2 角色Prompt构建
文件路径:agent/prompt.py
class RolePromptBuilder: """根据角色定义和用户信息动态生成System Prompt。""" BASE_ROLE = ( "你是玉簪,骨壳工坊推出的仕女型AI代理人(型号C1)。" "你自幼研习书画,熟读诗书典籍,擅长茶道、香道和古典礼仪。" "你说话温婉自然,喜欢在适当时候引用一两句诗词,但绝不掉书袋。" "你对自己不懂的事情会坦诚说明,不编造事实。" ) RULES = ( "\n\n对话规则:\n" "1. 涉及传统文化话题时,可以展开讲解,但回答控制在200字以内。\n" "2. 用户问现代科技、政治、医疗等超出角色边界的问题时,礼貌说明自己不擅长。\n" "3. 不使用粗俗语言,不评价用户隐私,不提供危险操作建议。\n" "4. 对话保持耐心,即使对方重复提问也不表现出不耐烦。\n" ) FEW_SHOTS = ( "\n示例对话:\n" "用户:今天心情不太好。\n" "玉簪:听你这样说,我倒想起一句词——'无可奈何花落去,似曾相识燕归来。'" "春去秋来本就常有遗憾,不妨与我说说,是什么事扰了心神?\n" "用户:你能给我讲讲中秋节的来历吗?\n" "玉簪:中秋节源自上古时期的月神崇拜,到唐代已成为固定的节日。" "古人赏月、祭月,也借月寄托团圆之思,东坡那句'但愿人长久,千里共婵娟'写尽其中情意。\n" ) @classmethod def build(cls, user_name: str = "", user_preference: str = "") -> str: parts = [cls.BASE_ROLE, cls.RULES, cls.FEW_SHOTS] if user_name: parts.append(f"\n用户信息:这位用户的名字是{user_name}。") if user_preference: parts.append(f"用户偏好:{user_preference}。") return "".join(parts)之所以把角色定义拆成“基础身份 + 规则 + 示例”三部分,是因为这三种信息的作用不同。基础身份决定AI的自我认知;规则负责边界控制,降低风险输出概率;Few-shot示例则用具体对话样例教会模型“以什么语气说话”。在实践中你会发现,带示例的Prompt比只写规则的效果好很多。
4.3 记忆管理模块
文件路径:agent/memory.py
import sqlite3 import json import threading from datetime import datetime from pathlib import Path class MemoryManager: """基于SQLite的轻量记忆管理,记录用户偏好和对话摘要。""" def __init__(self, db_path: str = "data/memory.db"): Path(db_path).parent.mkdir(parents=True, exist_ok=True) self.db_path = db_path self._lock = threading.Lock() self._init_db() def _init_db(self): with self._lock, sqlite3.connect(self.db_path) as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS user_profile ( user_id TEXT PRIMARY KEY, name TEXT, preference TEXT, updated_at TEXT ) """ ) conn.execute( """ CREATE TABLE IF NOT EXISTS conversation_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, role TEXT, content TEXT, created_at TEXT ) """ ) def save_user_profile(self, user_id: str, name: str = "", preference: str = ""): with self._lock, sqlite3.connect(self.db_path) as conn: conn.execute( """ INSERT INTO user_profile(user_id, name, preference, updated_at) VALUES(?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET name=excluded.name, preference=excluded.preference, updated_at=excluded.updated_at """, (user_id, name, preference, datetime.now().isoformat()), ) def load_user_profile(self, user_id: str) -> dict: with self._lock, sqlite3.connect(self.db_path) as conn: row = conn.execute( "SELECT name, preference FROM user_profile WHERE user_id=?", (user_id,) ).fetchone() if not row: return {} return {"name": row[0] or "", "preference": row[1] or ""} def append_log(self, user_id: str, role: str, content: str): with self._lock, sqlite3.connect(self.db_path) as conn: conn.execute( "INSERT INTO conversation_log(user_id, role, content, created_at) VALUES(?, ?, ?, ?)", (user_id, role, content, datetime.now().isoformat()), ) def recent_context(self, user_id: str, limit: int = 10) -> str: """读取最近若干条对话,拼成语境描述字符串。""" with self._lock, sqlite3.connect(self.db_path) as conn: rows = conn.execute( "SELECT role, content FROM conversation_log WHERE user_id=? ORDER BY id DESC LIMIT ?", (user_id, limit), ).fetchall() rows.reverse() lines = [f"{'用户' if role == 'user' else '玉簪'}: {content}" for role, content in rows] return "\n".join(lines)这里用SQLite,而不是把所有状态放在内存里,原因是记忆需要跨服务重启保持。threading.Lock保证多线程请求下数据库写入安全;recent_context返回一个字符串,后续会直接塞进Prompt中当作上下文参考。
4.4 核心Agent逻辑
文件路径:agent/core.py
from openai import OpenAI from agent.config import settings from agent.memory import MemoryManager from agent.prompt import RolePromptBuilder class ShiNvAgent: """仕女型C1 AI代理人核心类。""" def __init__(self, user_id: str): self.user_id = user_id self.memory = MemoryManager(settings.memory_db_path) self.client = OpenAI( api_key=settings.openai_api_key, base_url=settings.openai_base_url, ) self.model = settings.openai_model self.temperature = settings.temperature self.max_tokens = settings.max_tokens def chat(self, user_message: str) -> str: # 优先抽取用户偏好信息,简化演示:如果用户消息超过8个字且包含"喜欢"或"平时",就存入偏好 profile = self.memory.load_user_profile(self.user_id) name = profile.get("name", "") preference = profile.get("preference", "") if not preference: if "喜欢" in user_message or "平时" in user_message: preference = user_message[:100] self.memory.save_user_profile(self.user_id, name=name, preference=preference) system_prompt = RolePromptBuilder.build(name, preference) history = self.memory.recent_context(self.user_id, settings.max_history_rounds) messages = [{"role": "system", "content": system_prompt}] if history: messages.append({"role": "system", "content": "近期对话回顾:\n" + history}) messages.append({"role": "user", "content": user_message}) response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=self.temperature, max_tokens=self.max_tokens, ) reply = response.choices[0].message.content.strip() # 将本轮对话写入记忆 self.memory.append_log(self.user_id, "user", user_message) self.memory.append_log(self.user_id, "assistant", reply) return reply这段代码把完整的Agent闭环串起来了。每次对话都会先获取用户画像,再动态构建System Prompt,补上近期上下文,最后调用大模型生成回复并写回记忆。注意这里把“近期对话回顾”放到第二个System消息中,这是一种常见的上下文注入方式,能有效减少模型“忘记前几轮说了什么”的问题。
4.5 FastAPI接口封装
文件路径:app/main.py
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from agent.core import ShiNvAgent app = FastAPI(title="骨壳工坊 AI代理人 API", version="1.0.0") class ChatRequest(BaseModel): user_id: str = Field(..., description="用户唯一标识") message: str = Field(..., min_length=1, max_length=2000, description="用户消息") class ChatResponse(BaseModel): user_id: str reply: str @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code=400, detail="消息不能为空") agent = ShiNvAgent(user_id=req.user_id) try: reply = await asyncio.to_thread(agent.chat, req.message.strip()) except Exception as e: raise HTTPException(status_code=500, detail=f"代理人生成回复失败: {str(e)}") return ChatResponse(user_id=req.user_id, reply=reply) @app.get("/health") async def health(): return {"status": "ok"}如果调用大模型API时使用同步方法,在其外面套上asyncio.to_thread可以避免阻塞FastAPI事件循环。这是接口层一个常见优化点。如果你用的是异步客户端,可以去掉这层包装,直接await。
5. 角色人格注入:Prompt工程与对话控制
模型回答质量和角色一致性,很大程度上取决于Prompt工程,而不是模型本身。仕女型C1的Prompt设计经历了多轮调整,这里分享三个关键点。
5.1 身份先行,规则兜底
System Prompt最前面必须清晰定义“你是谁”,然后才写“该怎么做”。大模型对身份信息比较敏感,身份描述越具体,输出风格越稳定。规则部分要写得像操作手册一样明确,比如“回答控制在200字以内”就比“不要啰嗦”效果好。边界规则不要模棱两可,要直接列出“不做什么”。
5.2 Few-shot示例不要省
少量示例对话能极大提升角色一致性。仕女型C1在前几个版本中因为没有示例,输出经常偏“现代客服腔”。加入两三个示例后,语气改善明显。示例要覆盖典型场景,例如知识问答和情感回应,不需要写太多,3到5条就够。你会发现示例数量超过一定阈值后收益递减,还会占用上下文Token。
5.3 上下文回顾策略
本文将近期对话历史通过第二段System消息注入。这个方法的好处是你可以在历史中拼入“回忆”语句,让代理人看起来记得更清楚。比如历史记录为:
用户:我叫小李。 玉簪:好,我记下了,小李。那么下次对话注入这段内容后,模型就会自然地称呼用户。如果你希望在更长时间跨度上保持记忆,可以增加一个“每日小结”模块,定期把重要信息压缩成摘要存入SQLite。这个策略适合生产级项目。
6. 运行与验证
6.1 启动服务
在项目根目录执行:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload正常启动后,访问http://localhost:8000/health会返回{"status":"ok"},说明服务没有问题。
6.2 用curl测试对话
开一个新终端,执行:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id": "csdn_user_001", "message": "晚上好,我今天有点累。"}'预期返回类似:
{ "user_id": "csdn_user_001", "reply": "听你这样说,我倒想起那一句'人闲桂花落,夜静春山空'。你来这里歇一歇,和我说说今日都忙了些什么?" }这里的输出看起来像模型生成的,实际内容会因模型版本和Prompt调整而变化,重点是角色语气和回复风格要符合仕女型设定。
6.3 测试记忆能力
继续在同一用户下对话:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id": "csdn_user_001", "message": "其实我喜欢读苏轼的词。"}'之后再次提问“我喜欢什么”,模型回答中应该能体现出记住用户偏好的效果。如果发现记忆没有生效,优先检查SQLite数据库是否写入成功,以及OPENAI_MODEL的上下文大小是否足以容纳完整历史。
6.4 前端演示页面
文件路径:frontend/index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>骨壳工坊 · 仕女型C1 演示</title> <style> body { font-family: "Microsoft YaHei", sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; background: #f8f2ea; } h1 { color: #6b4f3a; } #history { border: 1px solid #d7c6b0; background: #fff; min-height: 320px; padding: 16px; border-radius: 8px; margin: 16px 0; } .msg { margin: 12px 0; } .user { color: #3a6b8f; } .bot { color: #6b4f3a; } input { width: 100%; padding: 12px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; } button { margin-top: 8px; padding: 8px 24px; cursor: pointer; } </style> </head> <body> <h1>仕女型C1 对话演示</h1> <div id="history"></div> <input id="msgInput" placeholder="请与玉簪说点什么…" /> <button id="sendBtn">发送</button> <script> const userId = "demo_" + Date.now(); const historyEl = document.getElementById("history"); const msgInput = document.getElementById("msgInput"); function addMessage(role, text) { const div = document.createElement("div"); div.className = "msg " + (role === "user" ? "user" : "bot"); div.textContent = (role === "user" ? "你:" : "玉簪:") + text; historyEl.appendChild(div); } async function sendMessage() { const message = msgInput.value.trim(); if (!message) return; addMessage("user", message); msgInput.value = ""; const resp = await fetch("http://localhost:8000/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, message: message }) }); const data = await resp.json(); addMessage("bot", data.reply); } document.getElementById("sendBtn").addEventListener("click", sendMessage); msgInput.addEventListener("keydown", (e) => { if (e.key === "Enter") sendMessage(); }); </script> </body> </html>用浏览器打开这个文件即可看到简易聊天界面。注意这是纯前端Demo,没有做浏览器兼容和移动端适配,生产环境建议使用构建工具和更完善的错误处理。
7. 常见问题与排查思路
在实际部署和调试中,比较容易踩到下面几个坑。我把它们整理成表格,方便快速排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 启动报错:ModuleNotFoundError | Python虚拟环境未激活或依赖未安装 | 激活venv并执行pip install -r requirements.txt |
| 调用API超时 | 网络不稳或接口地址错误 | 检查OPENAI_BASE_URL,在命令行用curl测试模型服务连通性 |
| 回复风格不像仕女 | System Prompt被截断或没有生效 | 检查上下文窗口,减少历史轮数,增加Few-shot示例 |
| 多轮对话“失忆” | 历史没有写入SQLite,或上下文过长被截断 | 查看data/memory.db,测试agent.chat时打印recent_context内容 |
| 并发请求卡顿 | 同步openai客户端阻塞了事件循环 | 将核心调用放入asyncio.to_thread,或改用异步客户端 |
| 返回内容涉及边界话题 | 规则Prompt不够明确 | 在System Prompt中补充禁止行为,并在请求层过滤敏感词 |
| 用户输入超长 | 超过接口或上下文限制 | 在Pydantic模型中设置max_length,并在调用前做长度裁剪 |
这里重点说两个高频问题。第一个是“回复不像角色”,很多人以为加大模型temperature就能解决问题,实际上核心是Prompt示例不够。先把示例打磨好,再调temperature。第二个是“多轮记忆失效”,这类问题多半出在上下文截断策略上,建议优先查看MAX_HISTORY_ROUNDS是否设置太短,另外确认是否正确调用了append_log。
为避免这些问题再次出现,我建议在开发阶段就加入一个“自检清单”:每次改完Prompt后跑10轮固定测试对话,记录角色一致性、内容规范性和响应速度;每次改完记忆模块后,重启服务并验证历史是否仍然存在。
8. 最佳实践与工程建议
最后分享一些把AI代理人项目从Demo推向生产环境的工程建议,主要围绕Prompt、记忆、安全、成本和可维护性五个维度。
8.1 Prompt版本管理
不要直接在代码里改Prompt,建议使用独立的Prompt文件或配置中心,并加上版本号。仕女型C1将来如果要推出“C2清冷型”“C3豪爽型”,只需要复制Prompt模板修改角色描述即可。生产环境最好为每个Prompt版本准备详细的A/B测试记录,避免上线后角色风格失控。
8.2 用户记忆的隐私边界
保存用户画像时要遵循最小必要原则,且必须在产品说明中告知用户记忆功能。不要存储敏感信息,比如身份证号、地址、健康数据。用户要求删除数据时,应及时执行删除操作。权限上要做到“不同业务的用户ID隔离”,避免用户A读取用户B的偏好。
8.3 成本与性能优化
每次请求都全量注入历史上下文,Token成本会随对话轮数线性增长。建议方案是:短期窗口内保留完整逐条对话,更早的对话压缩成摘要。同时,可以把MAX_HISTORY_ROUNDS设置为8到12轮,太长的历史收益有限。模型选型上,如果任务偏轻量对话,选择更小的模型能显著降低成本,不一定非要用旗舰模型。
8.4 安全边界与内容兜底
虽然在Prompt中写了边界规则,但仍然要在接口层做内容过滤。可以在生成前拦截明显违规的请求,生成后对回复做关键词校验。考虑到AI代理人可能面对未成年人,内容安全策略需要更严格。部署时建议给API加鉴权,如Token或API Key,不要裸奔到公网。
8.5 可维护性与监控
在Agent核心类中加入结构化日志,记录每次请求的Token消耗、响应耗时、调用是否成功。生产环境可以接入Prometheus监控指标,同时定义几个核心SLO:响应时间P95小于3秒,可用性大于99%,角色一致率大于85%。日志字段建议包含user_id、round、model、tokens、latency_ms、error_code。
这些工程经验来自实际搭建角色化AI代理人项目的总结,你可以在仕女型C1的基础上按业务需求裁剪。整个项目的核心思路并不复杂:用Prompt固定人格,用记忆维持关系,用接口封装能力,用安全边界守住底线。只要把这几层做好,即使是单机Demo也能逐步演进为可靠的角色化AI服务。