☰
多智能体小镇实战:从角色建模到AI静态图生成的完整闭环
2026/10/13 13:03:40 网站建设 项目流程

在 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.txt

Windows 用户的激活命令是.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.5

exposure表示该地点受天气影响的强弱。室内几乎不受日照影响,露天道路则接近 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", encoding="utf-8") as f: f.write(json.dumps(event, ensure_ascii=False) + "\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_id=agent.id, action=decision["action"], detail=decision.get("detail", ""), reason=decision.get("reason", ""), ) return self.memory.events[-len(self.agents):]

天气在每个时间步变化后,会直接影响所有室外角色的下一次决策。这就是“模拟感”的来源:同样的计划,在不同天气下会产生不同结果。七海在晴天会推迟出门,在阴天日照强度低的时候就会正常出发。

4.3 主流程:从模拟到画面

主程序把角色加载、世界循环、日志输出串起来:

def main(): agents = load_characters("config/characters") world = World( agents=agents, locations=load_locations("config/locations.yaml"), weather=WeatherState(temperature=32, sunshine=0.9, description="大晴天,太阳很大"), memory=MemoryStore("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_key=os.environ["IMAGE_API_KEY"], base_url=os.environ.get("IMAGE_BASE_URL"), ) response = client.images.generate( model=os.environ.get("IMAGE_MODEL", "your-image-model"), prompt=prompt, size="1024x1024", n=1, ) 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_ok=True) return os.path.join(base_dir, f"scene_{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 的分工边界。这个分支一旦跑顺,多智能体模拟的骨架就基本掌握了,后续加社交、加记忆、加可视化都只是在骨架上的扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询