MUD 这种以纯文字描述构建世界的“老古董”游戏,最近反而成了 AI Agent 开发者的新试验田。没有图形界面、没有标准化 API,只有一大堆半结构化的文字输出,玩家靠敲命令推进一切。让大模型替玩家刷 MUD 等级,表面看是游戏挂机问题,实际上是你把 LLM 接到真实外部环境后,必然要面对的感知、决策、执行、异常恢复这一整条链路。
很多人以为让 AI 玩 MUD,只要把每一轮游戏文本都丢给模型,让它输出“下一步指令”就行。真实跑一次就会发现,这种“每一步都问模型”的模式撑不了几分钟:延迟高、token 消耗快、模型经常输出无效命令,哪怕是最简单的打老鼠任务也会在某个循环里卡死。DeepSeek Harness 这类 Agent 执行框架,真正解决的就是“模型决策”和“环境执行”之间的断层。
这篇文章会从工程拆解的角度,带你走一遍最小可复现实验:用 DeepSeek 或任意 OpenAI 兼容模型作为大脑,用一套 Harness 风格的执行器作为手脚,用一个 MUD 连接器收发文字,最终让 AI 生成动作脚本并执行。整个过程不需要图形界面,也不依赖具体游戏客户端,重点是把 Agent 的骨架搭出来。
先给结论:如果你只是把模型 API 接入项目,却没有设计工具边界、动作白名单和执行循环,AI 打 MUD 基本等于空转;真正让“自动战斗脚本”可控的关键,在于把模型的自由文本输出,限制为一套可校验、可回滚、可监控的动作原语。
1. 这篇文章真正要解决的问题
很多刚接触 AI Agent 的人会有一个误解:让 AI 做事,只要提示词写得足够好就行。放到 MUD 自动战斗这个场景里,这个误解会被放大得非常明显。你让模型输出“kill rat”,模型确实能输出,但它不知道这条命令是否真的发到了服务器,不知道攻击有没有生效,不知道血量是不是已经太低。换句话说,模型只负责“说”,不负责“做”,更不负责“确认做完了”。
MUD 自动战斗看起来是个娱乐场景,实际拆开是三层工程问题:
- 感知层:从 MUD 返回的大段文本里提取当前房间、血量、魔法值、敌人状态。
- 决策层:根据当前状态选择攻击、施法、使用道具、逃跑还是休息。
- 执行层:把模型给出的动作转换成 MUD 协议命令,发送出去并读取结果。
这三层缺一不可。DeepSeek Harness 类框架的价值,不是让模型变得更聪明,而是把这三层粘合在一起,形成一个可以循环运转的 Agent 系统。
还有一个容易被忽略的问题:成本。如果每一步都调用大模型,刷一个等级可能消耗几十万 token,而且每步等待好几秒。更合理的做法是让 AI 先生成一段“战斗脚本”,再由轻量执行器按照脚本循环执行,模型只在状态异常或脚本执行失败时介入。这个设计思想,与 Harness 中 Skill 和 Tool 的分工是一致的。
什么样的人适合读这篇文章?正在搭建 AI Agent、做自动化测试、研究游戏 AI、或者想从“聊天机器人”走向“自动化操作”的开发者,都可以从中找到可借鉴的方案。如果你期待的是绕过游戏服务条款、抓包改数据之类的“外挂方案”,那这篇文章不适合你。下文所有内容都基于一个前提:你拥有目标 MUD 服务器的合法使用权限,或只在本地测试环境验证。
2. 核心概念:DeepSeek Harness、Skill 与 MUD 自动战斗
不同社区项目对“Harness”的理解并不完全一致,有的把它叫作 Agent 框架,有的叫执行器,有的叫工作流引擎。为了不绑死在某个具体发行版上,我们先统一概念边界。
| 概念 | 通俗解释 | 对应到 MUD 自动战斗 |
|---|---|---|
| 模型 | 负责理解和生成文本的大语言模型 | 判断“该打哪个怪”“血量太低要不要撤退” |
| Harness | 连接模型和外部工具的调度框架 | 把模型输出的动作指令变成真实的网络操作 |
| Skill | 可以被模型调用的技能包或脚本 | 定义战斗脚本的输入输出格式、动作范围 |
| Tool / 插件 | 具体的外部功能接口 | MUD 连接器,负责收发游戏文本 |
| Agent | 模型 + Harness + Skill + 记忆的整体系统 | 能自己刷等级的虚拟玩家 |
这段拆解非常重要。很多人看 DeepSeek Harness 的时候,容易把它误认为是一个“更强的模型”。实际上,Harness 负责的是工程控制:模型说“我想攻击老鼠”,Harness 决定这条指令能不能执行、怎么执行、执行后如何把结果回传给模型。
放到 MUD 场景,SDK 或框架内部可能不叫 Harness,但只要它具备“模型调用 + 工具注册 + 循环执行”这三个能力,本质就是同一套模式。明白了这层,你换任何框架都只是换配置文件和 API 写法而已。
Skill 是这套体系里最容易理解错的概念。Skill 不是让模型写任意 Python 代码,而是给模型划定一个可操作空间。比如在 MUD 战斗 Skill 里,模型只能从attack、cast、use、rest、flee这几个动作里选。执行器只认这些动作,模型说什么“输出一段任意代码”都不会被执行。这个约束,是保证 Agent 安全运行的第一道防线。
小结论:学 Agent 不要先去背某个框架的命令行参数,先理解“模型负责决策、Harness 负责执行、Skill 负责限制行动边界”这三层关系。后面所有代码,都是围绕这三层展开的。
3. MUD 为什么适合验证 AI Agent
MUD 全称 Multi-User Dungeon,是一种通过文字描述房间、怪物、道具和玩家状态的联机游戏。玩家输入look查看周围,输入kill rat攻击老鼠,输入flee逃跑。它诞生于个人电脑还很原始的年代,却因为交互形式上天然适配文本模型,在 AI Agent 时代重新有了用武之地。
为什么说 MUD 是 AI Agent 的绝佳训练场?核心原因是它的输入输出都是文本,不需要图像识别、不需要 GUI 解析,大模型可以直接理解。但与此同时,它又没有简单到可以靠固定脚本一路通关:怪物会反击、房间之间会迷路、状态会有随机变化。这个“不确定的文本世界”,恰好能测试模型在动态环境中做决策的能力。
如果把“AI 刷 MUD 等级”当成一个技术任务,可以拆成四个阶段:
- 登录与初始化:连接服务器,进入角色,获取初始状态。
- 观察状态:读取当前房间描述、角色血量、魔法值、敌人列表。
- 执行战斗:根据状态生成动作序列,循环攻击目标。
- 异常处理:血量不足时撤退休息,迷路时回溯,战斗结束后寻找下一个目标。
传统脚本能完成前三个阶段,但遇到异常情况基本就崩了。AI 带来的增量价值在第四阶段:它可以理解“当前状态不对劲”,然后重新制定策略。比如模型发现角色血量只剩 10%,就会自动生成包含flee和rest的新脚本,而不是继续盲目攻击。
这也是本文选择 MUD 的原因:它不是最强的生产场景,却是最能说明 Agent 核心机制的最小范例。你在这个场景里跑通的能力,可以直接迁移到文本客服自动处理、运维日志巡检、浏览器自动化等实际项目中。
4. 环境搭建与前置条件
开始写代码之前,先把环境准备好。本实验不依赖任何商业游戏客户端,推荐使用本地或私有部署的开源 MUD 服务端。确保有合法测试账号,不要在正式运营服务器上做这种实验。
建议环境如下:
- Python 3.10 及以上,支持 asyncio。
- 一个可访问的模型接口:DeepSeek API,或本地部署的 OpenAI 兼容服务。
- 一个 MUD 服务器地址,通常是 Telnet 协议,默认端口常见为 4000 等。
- 网络连通性:本机访问 API 和 MUD 服务器都必须通。
先创建虚拟环境并安装依赖:
python -m venv .venv source .venv/bin/activate # Windows 下使用:.venv\Scripts\activate pip install -U pip pip install telnetlib3 requests pyyaml如果你的 DeepSeek Harness 项目本身有官方安装脚本,请以项目 README 为准。这里安装的telnetlib3是 MUD 连接器依赖,requests是模型接口调用依赖,pyyaml用于读取 Harness 配置。
然后配置环境变量:
export DEEPSEEK_API_KEY="你的API Key" export DEEPSEEK_BASE_URL="https://api.deepseek.com/v1" export DEEPSEEK_MODEL="deepseek-chat" export MUD_HOST="127.0.0.1" export MUD_PORT="4000" export MUD_ROLE="test_warrior"如果你希望通过本地模型离线运行,只需把DEEPSEEK_BASE_URL换成本地服务地址,例如http://127.0.0.1:8000/v1。只要这个本地服务提供 OpenAI 兼容接口,下面的代码不需要改动。
Windows 用户需要特别注意目录权限。社区里经常遇到SetNamedSecurityInfoW failed (win32)错误,多数是因为工作目录位于需要高级权限的路径,或者程序尝试修改某个文件的 ACL。建议把所有实验代码放在用户目录下,例如C:\Users\你的用户名\mud-agent,不要放在C:\Program Files这类系统保护目录里。
5. 核心流程拆解:让 AI 生成战斗脚本,而不是每条指令都问模型
这是整个方案里最关键的设计判断。很多 Agent 教程教的是“ReAct 模式”:模型观察、模型思考、模型行动,循环往复。这种模式适合问题比较复杂、每步都需要重新推理的场景,但不适合 MUD 自动战斗。
MUD 战斗的特点是重复动作多、状态变化快、对延迟敏感。每打一只老鼠都要调用一次模型,既不经济也不稳定。更好的结构是两层设计:
- 第一层:策略生成器。模型根据角色状态和敌人信息,生成一段动作脚本。
- 第二层:循环执行器。执行器按脚本里的动作原语循环执行,不需要每步都问模型。
- 第三层:异常介入。当执行结果与预期不符,或角色状态发生变化时,再调用模型重新生成脚本。
这样的 Agent 才是真正“能干活”的系统,而不是一个每次都要想半天的聊天机器人。
具体到 MUD 场景,动作原语可以这样定义:
| 动作 | 对应 MUD 命令 | 说明 |
|---|---|---|
| attack | kill 目标名 | 攻击当前敌人 |
| cast | cast 技能名 目标名 | 使用法术 |
| use | use 道具名 | 使用道具 |
| rest | rest / sleep | 休息或睡觉恢复 |
| flee | flee | 从战斗中逃跑 |
模型不直接输出任意命令,只负责从这些动作中选择并组合成脚本。脚本格式可以设计成 JSON,例如:
{ "actions": [ {"action": "attack", "target": "rat"}, {"action": "attack", "target": "rat"}, {"action": "rest"} ] }这样设计有三个好处:
- 安全:模型无法让 Agent 执行白名单之外的操作。
- 可控:执行器能对每个动作做参数校验。
- 可回滚:每一轮生成的脚本都可以存档,出了问题可以切回之前的版本。
执行循环不需要复杂,核心逻辑就是“读取状态 -> 生成脚本 -> 执行脚本 -> 校验结果”。如果结果里出现“你已死亡”“你不在这里”“你的血量不足”等异常信息,就停止当前脚本,重新让模型规划。
6. 完整示例代码实现
下面给出一个最小可运行的工程骨架。代码不依赖特定 DeepSeek Harness 发行版,核心结构是通用的:连接器负责收发 MUD 文本,配置负责描述模型和环境,主循环负责任务调度。你完全可以把这里的思路迁移到任何 Agent 框架里。
6.1 第一步:实现 MUD 连接器
文件:mud_connector.py
import asyncio import telnetlib3 class MudConnector: def __init__(self, host: str, port: int, prompt_marker: str = ">"): self.host = host self.port = port self.prompt_marker = prompt_marker self.reader = None self.writer = None async def connect(self): # 建立 Telnet 连接 self.reader, self.writer = await telnetlib3.open_connection( self.host, self.port, cols=120 ) # 读取欢迎页和初始文本 initial = await self.read_until(self.prompt_marker, timeout=5) return initial async def send(self, command: str) -> str: # MUD 命令通常以换行结尾 self.writer.write(command.rstrip("\r\n") + "\r\n") # 读取服务端返回,直到出现命令提示符或超时 return await self.read_until(self.prompt_marker, timeout=5) async def read_until(self, marker: str, timeout: float = 5.0) -> str: data = "" try: while marker not in data: chunk = await asyncio.wait_for(self.reader.read(1024), timeout=timeout) if not chunk: break data += chunk except asyncio.TimeoutError: pass return data这个连接器做的事情很简单:连接 MUD 服务器,发送命令,读取返回文本。代码里的prompt_marker是 MUD 客户端每行命令前的提示符,通常像>或HP:100>,你需要根据实际服务器调整。
注意read_until是简化版本,真实 MUD 服务器输出可能包含大量 ANSI 颜色码。如果返回文本里混入特殊字符,可以在连接器里加一层清洗函数,把\x1b[...m之类的字符去掉。
6.2 第二步:配置 Harness 与 Skill
文件:config.yaml
agent: model: deepseek-chat base_url: https://api.deepseek.com/v1 max_retries: 3 timeout_seconds: 30 mud: host: 127.0.0.1 port: 4000 prompt_marker: ">" skills: - mud_fight_skill这个配置把 Agent 的模型参数、MUD 服务器信息和技能列表分开管理。如果你使用本地模型,只需修改base_url和model两个字段。
文件:skills/mud_fight_skill.yaml
name: mud_fight_skill description: MUD 自动战斗动作脚本生成技能 input_schema: state_text: type: string description: MUD 当前房间、血量、敌人等文字状态 max_actions: type: integer default: 20 output_schema: actions: type: array description: 动作原语列表 items: action: string target: string allowed_actions: - attack - cast - use - rest - flee这不是某个具体框架的官方字段,而是一种通用的描述方式。你换成官方 Harness 格式时,核心概念是一样的:定义输入参数、输出格式、可允许的动作范围。allowed_actions是安全边界,比任何提示词都可靠。
6.3 第三步:Agent 主循环
文件:agent_loop.py
import asyncio import json import os import requests from mud_connector import MudConnector DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") ACTION_ALIAS = { "attack": "kill", "cast": "cast", "use": "use", "rest": "rest", "flee": "flee", } def call_llm(messages, max_tokens=800): resp = requests.post( f"{DEEPSEEK_BASE_URL}/chat/completions", headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}"}, json={ "model": DEEPSEEK_MODEL, "messages": messages, "temperature": 0.2, "max_tokens": max_tokens, "response_format": {"type": "json_object"}, }, timeout=30, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] def parse_action_script(text: str): try: return json.loads(text) except json.JSONDecodeError: return {"actions": []} async def run_action_script(conn: MudConnector, actions, max_rounds=20): logs = [] for step in actions[:max_rounds]: action = step.get("action", "") target = step.get("target", "") if action not in ACTION_ALIAS: logs.append(f"skip unknown action: {action}") continue command = ACTION_ALIAS[action] if target: command = f"{command} {target}" output = await conn.send(command) logs.append(f"> {command}\n{output}") await asyncio.sleep(0.5) return "\n".join(logs) async def main(): conn = MudConnector( host=os.getenv("MUD_HOST", "127.0.0.1"), port=int(os.getenv("MUD_PORT", "4000")), ) await conn.connect() await conn.send(os.getenv("MUD_ROLE", "test_warrior")) system_prompt = ( "你是 MUD 游戏中的战斗策略助手。" "根据当前状态生成动作原语 JSON,只能使用 attack/cast/use/rest/flee 动作。" '输出格式为:{"actions":[{"action":"attack","target":"rat"}]}' ) state_text = await conn.send("look") messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"当前状态:\n{state_text}\n请生成战斗动作脚本。"}, ] for round_no in range(3): llm_output = call_llm(messages) action_script = parse_action_script(llm_output) result = await run_action_script(conn, action_script.get("actions", [])) print(f"[round {round_no}] {result[-800:]}") state_text = await conn.send("look") messages.append({"role": "assistant", "content": llm_output}) messages.append( { "role": "user", "content": f"执行结果:\n{result}\n当前状态:\n{state_text}\n继续或停止?", } ) if not action_script.get("actions"): break if __name__ == "__main__": asyncio.run(main())这段代码包含三个关键逻辑:
第一,call_llm封装了模型调用。通过标准 requests 访问 OpenAI 兼容接口,不绑定任何特定 SDK。如果你的模型服务不支持response_format参数,直接删掉这一行即可。
第二,parse_action_script把模型输出解析成 JSON。模型并不可靠,可能输出多余解释文字,这里只保留合法 JSON 解析结果。解析失败时返回空 actions,避免程序崩溃。
第三,run_action_script是安全执行器。它不执行模型直接给出的字符串命令,而是把白名单动作映射成 MUD 命令。模型说attack就映射为kill,说rest就映射为rest。模型永远无法跳出白名单。
6.4 第四步:运行命令
运行时先确认环境变量已经设置,然后执行:
python agent_loop.py如果一切正常,你会看到类似下面的输出:
[round 0] > kill rat 你攻击老鼠,老鼠反击但伤害不大。 > kill rat 你击败了一只老鼠。 > rest 你开始休息,体力逐渐恢复。这段输出只是示例。具体文本完全取决于你的 MUD 服务器。
7. 运行结果与效果验证
跑完一轮后,不要只看模型有没有输出,要验证三件事。
第一,命令是否真的发到了 MUD 服务器。最简单的验证方式是看连接器返回的文本。每执行一条命令,日志里都应该记录> 命令和对应的服务器响应。如果只有命令没有响应,说明read_until的提示符配置有问题,或者连接已经断开。
第二,角色状态是否在朝预期方向变化。战斗脚本执行后,角色经验值、怪物数量、血量状态应该有变化。你可以手动在 MUD 里look或score,也可以让 Agent 在每轮循环后自动拉取状态文本并打印摘要。
第三,异常时模型是否能介入。故意让脚本打一个不存在的目标,观察执行结果里是否出现类似“这里没有这个目标”的文本。如果执行器把这些文本传回给模型,模型应该能意识到脚本有问题,并生成新的动作序列。如果模型直接忽略异常继续执行,就要检查 messages 里异常文本是否被正确放到了用户消息里。
更严谨的验证方式是加日志。建议把每一轮的以下信息写入本地文件:
时间戳 当前状态文本 模型生成的原始输出 解析后的动作脚本 执行结果文本 是否触发异常介入有了这些日志,你才能判断某个回合失败时,是模型决策错了、解析失败了、还是执行器命令打错了。没有日志的 Agent 实验,出了问题几乎无法排查。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Python 包无法安装 | 网络源不可用或 Python 版本不匹配 | 查看完整报错和 pip 版本 | 换国内镜像源,升级或降级 Python 版本 |
Windows 报SetNamedSecurityInfoW failed (win32) | 工作目录处于受保护路径,或文件 ACL 设置失败 | 检查报错堆栈涉及哪个目录 | 将项目移动到用户目录,以普通用户运行 |
| Skill 读取文件权限异常 | 服务用户没有目标目录读取权限 | 查看进程用户和目录所有者 | 调整权限,或在容器内以固定 UID 运行 |
| 模型接入失败或超时 | Base URL / API Key 配置错误 | curl 测试模型接口连通性 | 核对环境变量,确认模型服务兼容 OpenAI 接口 |
| 模型返回内容无法解析为 JSON | temperature 过高或提示词约束不足 | 打印原始模型输出 | 降低 temperature,增加 few-shot 示例,启用 JSON mode |
| 战斗脚本卡死不动 | 连接器等待提示符超时,或脚本没有退出条件 | 查看日志里最后一条发送命令 | 设置最大执行轮次,增加异常关键词检测 |
| 本地模型生成的动作不准确 | 小参数模型指令遵循能力弱 | 对比 deepseek-chat 与本地模型输出 | 改用更大模型,或把动作候选直接写进提示词 |
| 想回退到之前的脚本 | 没有版本管理 | 检查 skills 目录是否有 git 记录 | 所有 Skill 文件纳入版本控制,回退时切 tag |
这里特别说明一下 Windows 权限问题。SetNamedSecurityInfoW是 Windows 系统用来修改文件或目录安全描述符的底层 API。很多工具在安装插件、写入配置时会调用它。如果返回失败,通常不是因为代码写错了,而是当前进程没有对目标路径设置 ACL 的权限。最容易的解决办法就是不要把这些文件放在系统盘受保护目录,也不要用管理员权限强行运行。保持最小权限运行,反而能避开这类问题。
另一个高频问题是“DeepSeek Harness 能不能完全离线运行”。答案是能。MUD 连接器和执行器本来就是本地进程,唯一依赖外部的是模型接口。把模型部署到内网服务器或局域网机器上,再把配置里的base_url指向内网地址,整个系统就不需要访问公网了。
9. DeepSeek Harness 的最佳实践与工程建议
如果你把上面这套骨架用在实际项目中,下面的几条建议值得认真考虑。
第一,动作白名单是底线。模型输出天然不可控,你可以在提示词里写一万遍“不要执行危险命令”,都不如执行器里只允许attack/cast/use/rest/flee五个动作来得可靠。任何 Agent 工程都应该先定义动作边界,再开放模型调用。
第二,每次实验都要可回放。Agent 的失败往往不是瞬间发生的,而是经过多轮交互累积出来的。建议保存每一轮的状态快照、模型输入、模型输出、执行结果。出了问题的时候,用这部分数据做回放对比,才能找到是决策层的问题还是执行层的问题。
第三,Skill 文件要纳入版本管理。热词里经常出现“deepseek harness 代码回退”,说明很多人在调试 Agent 时会频繁修改 Skill 配置。不要直接改线上文件,先把改造版复制成skill_v2.yaml,跑通后再替换。每次替换都打一个 tag,回退成本会低很多。
第四,模型选择要分场景。如果只是快速验证流程,优先用 DeepSeek API,省心且稳定。如果数据敏感或需要完全内网运行,再考虑本地模型。本地模型的优势是隐私和成本可控,但小模型的指令遵循能力可能不够,战斗脚本生成结果会明显差一截。建议先跑通流程,再根据自己的需求权衡换哪个模型。
第五,尽量隔离测试环境。MUD 服务器如果是远程正式服,任何一条错误命令都可能造成不可逆后果。最好把服务端跑在本地 Docker 容器里,Agent 先在这个环境里模拟上千次战斗,验证稳定后再考虑更复杂的环境。生产环境里永远不要跑没有经过回放测试的新脚本。
第六,多 Agent 协作是后续方向。单 Agent 做 MUD 战斗已经能闭环,但遇到探索地图、买卖装备、组队配合这类任务时,一个 Agent 会手忙脚乱。后期可以让一个 Agent 负责战斗指挥,另一个 Agent 负责地图记忆和目标规划,再通过 Harness 的调度机制把两者连接起来。这套思路比堆提示词要可靠得多。
10. 总结与后续学习方向
这篇文章并不是在教你“刷等级”,而是用 MUD 自动战斗这个场景,把 Agent 自动化中最重要的机制讲清楚:模型负责决策,Harness 负责执行,Skill 负责限制边界。没有这套分层,任何大模型都只能停留在“聊天”层面,无法真正操作系统。
你可以继续深入的方向有几个。
一是把状态摘要升级成结构化状态。目前只是把原始文本直接交给模型,效果还不够稳定。更合理的方式是从文本里抽取 HP、MP、坐标、敌人列表,用 JSON 格式传给模型。
二是扩展动作原语。除了战斗,还可以增加move、buy、sell、give等动作,让 Agent 能完成更复杂的任务流。
三是引入多个 Specialist Agent。一个模型负责战斗操作,另一个模型负责地图导航,第三个模型负责异常判断。你不需要一次性把系统做得很复杂,但理解了这个方向,后续的扩展空间会大很多。
最后再强调一遍边界。AI 自动战斗脚本能跑通,不代表可以随意用在正式运营的游戏中。更好的玩法是搭一个本地 MUD 服务端,把整个工程当作一次 Agent 设计实验。你在这次实验里积累的日志、权限、回滚、异常恢复经验,才是真正可以迁移到生产项目里的资产。