ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多智能体小镇实战:从角色建模到AI静态图生成的完整闭环

多智能体小镇实战:从角色建模到AI静态图生成的完整闭环 在 AI Agent 应用开发中比单个智能体更有意思的是多智能体之间的互动模拟。AI 小镇AI Town是这类项目的典型形态多个角色被放进同一个虚拟空间各自拥有性格、记忆、日程和人际关系系统按时间步推进它们的行为再让大模型补充计划外的反应。“七海去找星瞳玩但是路上太晒了”这个场景正好覆盖了多智能体模拟里的几个核心环节出行意图、环境状态、决策调整和画面生成。这篇文章会以一个开源的 AI 小镇项目 my_ai_town 为参考从工程实践角度拆解如何在本地搭建多智能体小镇如何让角色在“太阳太晒”的条件下修正计划以及如何把模拟结果输出成 AI 静态图。项目开源地址为https://github.com/mewamew/my_ai_town并提供了 macOS 和 Windows 两个平台的本地运行方式。文章会先讲清楚这类模拟的基本原理再给出环境准备、数据建模、决策实现、图像生成和问题排查的完整链路。适合正在做 AI Agent 开发、AI 应用开发或者想学习多智能体模拟、提示词工程的角色互动项目开发者阅读。看完后你可以得到一个包含角色配置、环境状态、决策日志和静态图生成的最小闭环。1. 先理解“AI 小镇”式模拟的核心机制1.1 从一个标题反推技术需求把“七海去找星瞳玩但是路上太晒了”拆开看它实际上包含四个必须被程序表达的信息“七海”和“星瞳”两个可交互的 AI 角色需要各自的身份描述、性格倾向和行动偏好。“去找…玩”一个带有目的地和动机的行动计划而不是一次随机的聊天。“路上太晒了”环境状态天气、日照强度必须能进入决策逻辑否则角色不会受到“晒”的影响。“AI 静态”最终产物是一张静态插画不是实时动画或视频流。所以这个项目至少要包含四个模块角色管理、环境状态、决策引擎、图像生成。四个模块之间用事件日志串联模拟产生的每个动作都有时间戳、角色、动作和原因后续图像生成才能从日志里提取场景要素。1.2 什么是多智能体模拟多智能体模拟Multi-Agent Simulation是指多个 AI 角色在共享环境中按照各自的内部状态和外部条件逐步产生行动序列的过程。它和“一次对话就结束”的 ChatBot 不同重点在于状态持续变化角色有记忆、有当前位置、有日程环境有天气、有时间、有地点关系系统按固定时间步推进每一轮都会产生新事件。这类项目最早的公开代表是斯坦福提出的生成式智能体Generative Agents研究后来社区里出现了大量“AI 小镇”实现。它们通常把每个角色建模成“性格摘要 记忆流 每日计划”的组合。角色不是背台词而是随时间和环境调整行动。标题里的“路上太晒”就是一个典型的环境干扰计划是出门但环境条件导致出门这个动作需要被推迟或改变。1.3 规则驱动和 LLM 驱动需要混合使用一个常见误区是把所有决策都交给大模型认为只要提示词写得好角色就会自动演好故事。实际操作中完全依赖 LLM 会带来两个问题第一模型每次输出有随机性角色会频繁“精分”上午决定出门下午又忘记这个计划第二LLM 调用有延迟和成本每个时间步都让模型重新决策模拟速度会非常慢。推荐做法是规则优先、LLM 补全。像“当前时间是否在日程范围内”“天气是否超过角色耐晒阈值”“目标地点是否存在”这类条件用代码判断像“遇到熟人会说什么”“看到路边小吃摊会不会改变路线”这类开放内容再交给 LLM 生成。这样既保证模拟稳定又保留角色表达的自然度。2. 环境准备本地跑通一个多智能体小镇2.1 环境清单项目要求说明操作系统macOS 或 Windows项目提供两个平台的本地运行方式Python3.10 及以上主要开发语言建议使用虚拟环境Git2.x用于克隆代码和切换版本大模型服务OpenAI 兼容接口或本地模型负责角色行为、记忆概括等文本生成图像生成服务图片生成接口或本地扩散模型负责把场景提示词渲染成静态图可选工具Ollama、vLLM 或 ComfyUI本地部署大模型或图像模型时使用如果没有服务端 API也可以先用本地模型跑通文本部分图像部分可以延迟到后面再接入。关键是先把模拟循环跑通不要一开始就追求完整链路。2.2 克隆项目与基础依赖git clone https://github.com/mewamew/my_ai_town.git cd my_ai_town python -m venv .venv source .venv/bin/activate pip install -r requirements.txtWindows 用户的激活命令是.venv\Scripts\activate。安装依赖后可以先运行一个自检命令确认大模型接口连通。如果原始仓库的 README 里提供了启动脚本优先使用仓库脚本如果提示缺少依赖再根据报错补装。2.3 目录结构设计参考常见 AI 小镇工程一个可维护的项目通常按职责拆分。这里给出最小可运行的结构实际仓库可能不同落地前先看 README 和已有文件my_ai_town/ ├── config/ │ ├── characters/ │ │ ├── nanami.json │ │ └── xingtong.json │ ├── locations.yaml │ └── model.yaml ├── src/ │ ├── town/ │ │ ├── agent.py │ │ ├── location.py │ │ ├── weather.py │ │ ├── memory.py │ │ ├── planner.py │ │ └── event_log.py │ ├── image/ │ │ ├── prompt_builder.py │ │ └── generator.py │ └── main.py ├── data/ │ ├── events.jsonl │ └── images/ └── requirements.txt这个结构把“数据定义”“模拟逻辑”“图像生成”三块分开。角色配置属于数据决策属于逻辑图片保存属于产物改动任意一块都不会影响另外两块。2.4 配置模型服务llm: provider: openai_compatible base_url: https://api.example.com/v1 api_key: ${LLM_API_KEY} model: your-chat-model temperature: 0.7 image: provider: openai_compatible base_url: https://api.example.com/v1 api_key: ${IMAGE_API_KEY} model: your-image-model size: 1024x1024这里的关键点是base_url允许你对接 OpenAI 兼容的服务也可以换成内网部署的网关。api_key不要直接写在文件里使用环境变量注入。temperature控制 LLM 输出的随机性。模拟角色行为时建议 0.7 左右太高容易角色失控太低则显得机械。图像模型的名称和size参数要以实际接口为准不同服务差异很大。配置完成后写一个最小的连通性测试验证文本接口和图像接口都能返回结果再继续后面的开发。3. 数据建模角色、地点、天气和记忆怎么设计3.1 角色配置不能只有一句人设角色文件如果只写“七海是一个可爱的角色”决策引擎完全无法使用这条信息。角色建模需要拆成可计算字段。{ id: nanami, name: 七海, home: nanami_home, persona: 喜欢和朋友待在一起方向感一般怕晒讨厌烈日, sun_tolerance: 0.3, daily_plan: [ { time: 09:00, action: 起床 }, { time: 10:00, action: 准备出门 }, { time: 10:30, action: 去星瞳家 } ], relations: { xingtong: 好姐妹想去她家玩 } }其中sun_tolerance是一个量化字段取值范围 0 到 1表示角色能承受的最大日照强度。当天气状态的日照值大于这个阈值时决策引擎就知道角色会觉得“太晒”。这个字段非常关键它让“怕晒”从一段描述文字变成了可比较的数字规则代码可以直接判断。3.2 地点与天气状态地点用 YAML 配置方便人工维护。每个地点可以定义暴露程度也就是受天气影响的程度。locations: nanami_home: name: 七海的小屋 exposure: 0.0 xingtong_home: name: 星瞳的家 exposure: 0.1 road_to_xingtong: name: 去星瞳家的路 exposure: 0.9 town_square: name: 小镇广场 exposure: 0.5exposure表示该地点受天气影响的强弱。室内几乎不受日照影响露天道路则接近 1。角色在一个地点感受到的实际日照强度由全局天气乘以其暴露程度计算得到。天气状态本身用 Python 数据类表达from dataclasses import dataclass dataclass class WeatherState: temperature: int sunshine: float description: str def effective_sunshine(self, exposure: float) - float: return round(self.sunshine * exposure, 2)sunshine是 0 到 1 的日照强度effective_sunshine计算某个地点下角色实际感受到的日照。比如全局日照 0.9露天道路暴露 0.9实际日照就是 0.81明显超过七海 0.3 的耐晒阈值。3.3 记忆和事件日志记忆是让角色“记得自己刚才想干什么”的关键。最基本的实现是事件日志加摘要import json from datetime import datetime class MemoryStore: def __init__(self, path: str): self.path path self.events [] def add(self, agent_id: str, action: str, detail: str, reason: str ): event { time: datetime.now().isoformat(), agent: agent_id, action: action, detail: detail, reason: reason, } self.events.append(event) self._append_to_file(event) def _append_to_file(self, event: dict): with open(self.path, a, encodingutf-8) as f: f.write(json.dumps(event, ensure_asciiFalse) \n) def recent(self, agent_id: str, limit: int 5): filtered [e for e in self.events if e[agent] agent_id] return filtered[-limit:]事件日志既用于调试也用于生成静态图的提示词。日志里必须包含agent、action、detail、reason四个字段reason特别重要它是“为什么这样做”的直接记录也是后续图像提示词里最有画面感的信息。4. 实现“去找朋友但路上太晒”的决策链路4.1 行动规划规则优先LLM 补全决策引擎不能直接让 LLM 从零决定而是先检查日程再检查环境最后交给 LLM 补全表达。class Planner: def __init__(self, llm, memory: MemoryStore): self.llm llm self.memory memory def decide(self, agent, world): plan agent.get_current_plan(world.clock) if plan is None: return {action: idle, reason: 当前没有日程安排} if plan.action go_visit: return self._handle_visit(agent, world, plan) return plan.to_dict() def _handle_visit(self, agent, world, plan): target world.locations[plan.target] sunshine world.weather.effective_sunshine(target.exposure) if sunshine agent.sun_tolerance: return { action: delay_visit, original_target: plan.target, detail: f{agent.name}站在门口看了一眼外面的太阳决定晚点再出发, reason: f路径日照强度 {sunshine}超过角色耐晒阈值 {agent.sun_tolerance}, } return { action: go_visit, target: plan.target, detail: f{agent.name}带上小包往{target.name}的方向走去, reason: f日照强度 {sunshine} 在可接受范围内, }这个实现的关键在于“决策依据可解释”。角色推迟出行不是因为提示词里写了“可能推迟”而是因为0.81 0.3这个条件成立。规则代码先拦截确定性事实LLM 只负责把事实翻译成自然语言描述。4.2 环境状态如何进入世界循环世界对象负责推进时间、更新天气、驱动角色行动class World: def __init__(self, agents, locations, weather, memory): self.clock 0 self.agents agents self.locations locations self.weather weather self.memory memory self.planner Planner(self._llm, memory) def tick(self): self.clock 1 # 每 3 个时间步随机小幅变化天气 if self.clock % 3 0: self.weather.sunshine max(0.0, min(1.0, self.weather.sunshine 0.1)) def step(self): self.tick() for agent in self.agents: decision self.planner.decide(agent, self) self.memory.add( agent_idagent.id, actiondecision[action], detaildecision.get(detail, ), reasondecision.get(reason, ), ) return self.memory.events[-len(self.agents):]天气在每个时间步变化后会直接影响所有室外角色的下一次决策。这就是“模拟感”的来源同样的计划在不同天气下会产生不同结果。七海在晴天会推迟出门在阴天日照强度低的时候就会正常出发。4.3 主流程从模拟到画面主程序把角色加载、世界循环、日志输出串起来def main(): agents load_characters(config/characters) world World( agentsagents, locationsload_locations(config/locations.yaml), weatherWeatherState(temperature32, sunshine0.9, description大晴天太阳很大), memoryMemoryStore(data/events.jsonl), ) for _ in range(10): world.step() scene build_scene_from_events(world.memory.events) prompt build_prompt(scene) image_path generate_image(prompt, data/images/scene_001.png) print(f场景图已保存: {image_path})这里用build_scene_from_events把最近事件汇总成一个场景对象再交给提示词构造器和图像生成器。注意模拟循环和图像生成是分离的模拟可以跑很多轮但只有选出关键事件后才生成图片避免每次调用图像接口造成高成本。5. 把模拟结果渲染成 AI 静态图5.1 从事件日志构造场景提示词静态图的质量取决于提示词能否把日志里的结构化信息完整表达出来。直接拼接日志文本效果很差建议先整理成场景对象再生成提示词。def build_scene_from_events(events): latest events[-1] return { characters: latest[agent], action: latest[detail], reason: latest.get(reason, ), weather: latest.get(weather_context, 晴天), location: latest.get(location, 小镇), }提示词构造器需要固定风格前缀再动态拼接场景要素def build_prompt(scene: dict) - str: base_style ( 动漫风格插画明亮通透的日式小镇场景 角色表情自然构图具有生活气息 ) content ( f角色{scene[characters]} f地点{scene[location]} f天气{scene[weather]} f正在进行的动作{scene[action]} f画面理由{scene[reason]} ) return base_style content 画面干净细节丰富竖构图。注意提示词里的“画面理由”非常重要。它来自日志里的reason字段例如“路径日照强度 0.81超过角色耐晒阈值 0.3”图像模型会据此画出强烈的阳光、角色的遮挡动作或犹豫表情让静态图有叙事感。5.2 调用图像生成接口图像生成统一封装在一个模块里方便替换服务商import os from openai import OpenAI def generate_image(prompt: str, save_path: str) - str: client OpenAI( api_keyos.environ[IMAGE_API_KEY], base_urlos.environ.get(IMAGE_BASE_URL), ) response client.images.generate( modelos.environ.get(IMAGE_MODEL, your-image-model), promptprompt, size1024x1024, n1, ) image_url response.data[0].url download_image(image_url, save_path) return save_path代码里的模型名称故意写成环境变量。不同图像服务的模型名、请求格式、返回字段差异很大落地前必须确认实际接口文档。如果你的图像服务返回的是 base64 内容而不是 URL就要改成解析b64_json。5.3 图片保存与命名图片保存建议按日期或模拟轮次命名避免覆盖from datetime import datetime def make_image_path(base_dir: str) - str: ts datetime.now().strftime(%Y%m%d_%H%M%S) os.makedirs(base_dir, exist_okTrue) return os.path.join(base_dir, fscene_{ts}.png)生成完成后除了保存图片还要把对应的提示词和事件日志一并保留。这样回头排查“为什么生成出来不是想要的画面”时可以直接对比日志、提示词和实际图片三者定位是哪一环出了问题。6. 运行验证从日志到图片的完整闭环6.1 启动方式以最小验证目标运行python src/main.py首次运行建议只用两个角色、10 个时间步。时间太短看不到天气变化影响太长会产生大量重复日志不方便观察。6.2 预期输出正常运行的日志文件data/events.jsonl里应当出现类似记录{time: 2025-06-01T10:30:00, agent: nanami, action: delay_visit, detail: 七海站在门口看了一眼外面的太阳决定晚点再出发, reason: 路径日照强度 0.81超过角色耐晒阈值 0.3}如果天气在第 6 个时间步降到阴天日志后续应当出现七海正常出发的记录{time: 2025-06-01T11:00:00, agent: nanami, action: go_visit, detail: 七海带上小包往星瞳的家方向走去, reason: 日照强度 0.4 在可接受范围内}同时data/images/目录下会生成图片文件图片内容能明显看出晴天或转阴后的场景差异。6.3 验证清单验证项检查方式通过标准角色是否执行每日计划查看 events.jsonl 中的 action 字段起床、出门、拜访等动作按时间出现天气是否进入决策对比天气变化前后的 reason 字段日照超标时 action 变为 delay_visit日志是否完整检查每个事件是否含 time/detail/reason字段齐全无空值提示词是否正确打印 build_prompt 的输出包含角色、地点、天气、动作、理由图片是否保存检查 images 目录有非空 png 文件且尺寸正确重复运行是否稳定连续运行两次并对比规则分支结果一致LLM 补全允许差异注意只验证程序能启动不算完成。要确认角色确实因为日照阈值改变了行动并且变化体现在日志、提示词和最终图片三个层面。7. 常见问题排查7.1 按这条链路排查多智能体项目最忌讳直接看现象改逻辑。出场顺序应该是配置 - 日志 - 提示词 - 图片。先确认角色配置里的sun_tolerance和exposure数值合理再看事件日志里的action和reason是否符合预期然后检查build_prompt拼出的提示词有没有遗漏场景字段最后才怀疑图像模型本身。大部分“生成结果不对”的问题根源都在提示词或日志而不是图像模型。7.2 高频问题表问题现象常见原因检查方式处理建议角色无视天气直接出门决策代码没读取天气状态或sun_tolerance设得过大打印effective_sunshine和阈值确认天气传入 planner调低角色阈值日志里的 reason 为空模型输出格式不对或 detail 字段拼接失败打印原始模型返回增加 JSON 解析容错使用结构化输出图片里没有“太晒”的感觉提示词没有包含 reason 字段打印最终提示词在内容拼接中显式加入“画面理由”生成图片内容与事件不符提示词描述太泛对比日志和提示词把 location、action、reason 全部写进提示词模拟每步都调用 LLM速度很慢没有规则前置拦截查看调用日志把日程、天气、阈值判断改为规则代码7.3 三个容易踩的坑第一个坑是角色记忆无限增长。每次行动都追加事件几十轮之后记忆列表会非常长每次决策都把它塞进提示词既浪费 token 又让模型注意力分散。应该在达到上限时触发摘要把旧事件压缩成一句短期记忆只保留最近 N 条原始事件。第二个坑是完全依赖 LLM 补全带来幻觉。如果你让模型自由决定“七海会不会出门”它可能编造出完全不符合角色设定的事件比如角色突然出现在一个不存在的地点。规则代码必须先把所有确定性条件过滤完LLM 只负责润色和生成开放内容。第三个坑是图像提示词过载。场景里有一点细节就往提示词里塞角色、天气、动作、路边摊、云朵、衣服颜色全写进去图像模型反而抓不住重点。提示词应遵循“风格前缀 角色 地点 天气 动作 理由”的结构控制在一段话以内理由只保留最关键的一条。8. 最佳实践与扩展方向8.1 多智能体项目的可复用清单把这套工程经验沉淀成一份清单每做一个类似项目都可以先过一遍角色是否具备可计算的属性字段耐晒阈值、移动速度、社交偏好等。地点是否区分室内外暴露程度天气是否会随时间步变化。决策链路是否做到规则优先、LLM 补全。每个事件是否都记录action、detail、reason三个字段。记忆是否有容量上限和摘要压缩机制。静态图提示词是否从结构化场景对象生成而不是直接拼接原始日志。图像生成接口是否封装成独立模块方便替换服务商。模拟循环开头是否包含配置自检和环境检测。8.2 从学习环境到生产环境的差异本地跑通模拟和真正上线运行差别很大。学习环境只需要一个 Python 脚本和两个角色生产环境至少要补上几块配置外置化模型地址和密钥走环境变量或配置中心日志落盘并加上轮转避免events.jsonl无限膨胀增加调用限流和重试机制防止大模型接口抖动导致模拟中断对每次图像生成做成本控制和失败重试。如果项目面向 Web 用户还需要考虑并发模拟、角色数据隔离、以及图像生成结果的对象存储托管而不是直接存本地磁盘。8.3 下一步可以扩展的方向这个最小闭环稳定后值得扩展的方向有几个。一是把单角色决策升级为社交关系网络让角色之间的友好度、聊天记录影响后续行动比如星瞳听说七海要来会提前准备零食。二是接入语音或前端页面把事件日志实时渲染成可视化小镇地图用户能看到角色移动过程而不仅仅是一张静态图。三是用 Spring AI Alibaba 之类的 Java 大模型接入层重构服务端把模拟逻辑拆成独立服务便于和现有业务系统集成。四是给记忆系统增加向量检索让角色在决策时能“想起”与当前场景最相关的历史事件而不是简单取最近几条。对新手来说最有价值的练习不是先把代码写完美而是先跑通“天气导致行程变化”这一个分支理解规则与 LLM 的分工边界。这个分支一旦跑顺多智能体模拟的骨架就基本掌握了后续加社交、加记忆、加可视化都只是在骨架上的扩展。
返回列表