ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI代理人开发实战:用Prompt工程打造角色化仕女型C1

2026/9/26 8:27:52 拓冰建站 浏览量
AI代理人开发实战:用Prompt工程打造角色化仕女型C1 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服务。uvicornASGI服务器。pydantic参数校验与数据模型。python-dotenv读取环境变量。sqlite3Python标准库用于记忆持久化无需额外安装。具体版本不需要固定以你本机安装时的最新稳定版本为准。这里的关键是你本机已经有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_URLhttps://api.openai.com/v1 OPENAI_MODELgpt-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.pyimport 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.pyclass 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.pyimport 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(parentsTrue, exist_okTrue) 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 nameexcluded.name, preferenceexcluded.preference, updated_atexcluded.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.pyfrom 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_keysettings.openai_api_key, base_urlsettings.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, namename, preferencepreference) 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( modelself.model, messagesmessages, temperatureself.temperature, max_tokensself.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.pyfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from agent.core import ShiNvAgent app FastAPI(title骨壳工坊 AI代理人 API, version1.0.0) class ChatRequest(BaseModel): user_id: str Field(..., description用户唯一标识) message: str Field(..., min_length1, max_length2000, description用户消息) class ChatResponse(BaseModel): user_id: str reply: str app.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code400, detail消息不能为空) agent ShiNvAgent(user_idreq.user_id) try: reply await asyncio.to_thread(agent.chat, req.message.strip()) except Exception as e: raise HTTPException(status_code500, detailf代理人生成回复失败: {str(e)}) return ChatResponse(user_idreq.user_id, replyreply) 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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 idhistory/div input idmsgInput placeholder请与玉簪说点什么… / button idsendBtn发送/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. 常见问题与排查思路在实际部署和调试中比较容易踩到下面几个坑。我把它们整理成表格方便快速排查。问题现象常见原因解决思路启动报错ModuleNotFoundErrorPython虚拟环境未激活或依赖未安装激活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服务。