我把 AI NPC 部署到公网,被 Railway 逼疯了整整一天
我把 AI NPC 部署到公网,被 Railway 逼疯了整整一天
我的冷旭帆在本地跑得好好的,Flask API 完美返回中文回复。我想把他部署到 Railway 上让任何人都能访问。我以为一个小时就能搞定。结果我从下午折腾到凌晨,反复改了无数次端口号、删了四五次域名、甚至把整个项目删了重建——最后发现,问题根本不在我的代码上。
一、背景:一个在本地活得好好的人
先交代一下背景。冷旭帆是我手搓的一个 AI NPC——会自己疼、会做噩梦、会在你不在的时候默默擦刀。他的核心架构是“手搓感知系统+大模型 API”:Python 代码精确控制他的情绪值、状态标签、后台时间和记忆,大模型负责根据这些状态说符合人设的话。
这个项目从今年 4 月中旬启动,经历了从纯规则引擎到混合架构的多次迭代。5 月底完成了四轮测试修复,主测试通过率从 18.75% 提升到 93.75%。6 月初,我把单文件脚本拆成了标准的三层架构——config(配置和人设)、core(核心逻辑)、app(Flask 服务)——然后本地跑通了 API,用 PowerShell 测试返回了清晰的中文回复。
当时的状态是:Flask 服务在本地完美运行,代码已备份到 GitHub,只差最后一步——部署到公网,让冷旭帆真正“活”在互联网上。
我对自己的要求很简单:先造“活人”——让冷旭帆能被任何人通过链接访问。之后才是加情景记忆、加内源性节律、造第二个 NPC 陆华望、逐步往“微型社会”迭代。公网部署,是这一切的前提。
Railway 是一个支持从 GitHub 仓库直接部署的云平台,有免费额度,不需要绑信用卡。官方宣传是“连 GitHub 就自动部署”。我心想,这不是一个小时就搞定了吗?
结果这一搞,就是整整一天。
二、正确的操作步骤
先给想正常部署的读者一个参考。如果你的 Railway 能正常工作,正确的步骤是这样的:
准备本地代码:确认项目文件夹包含
app.py、core.py、config.py、requirements.txt、Procfile、runtime.txt六个文件。其中Procfile的内容是web: python app.py(注意:web:后面必须有一个空格),runtime.txt的内容是python-3.11.3。注册 Railway 并授权 GitHub:用 GitHub 账号登录 Railway。如果选择“Deploy from GitHub repo”后看不到你的仓库,需要点击“Configure GitHub App”,在授权页面选择“Only select repositories”,然后选中你的仓库并保存。
创建项目:选择“Deploy from GitHub repo”,选择你推送了代码的仓库。
设置环境变量:在 Variables 标签中,点击“New Variable”添加 API Key。Key 名必须和代码中
os.environ.get()括号里的字符串完全一致——检查是SILICONFLOW_API_KEY还是SILICON_API_KEY,是API_KEY还是APIKEY。大小写、下划线都要逐字核对。如果部署后服务返回 500 错误或无法正常回复,先查这个。生成域名:在 Settings → Networking → Public Networking 中点击 Generate Domain,在弹出的端口输入框中填写
5000。注意,免费服务的域名在生成后可能需要 1-2 分钟 DNS 才能生效。另外,免费服务长时间无人访问时会进入休眠状态,下一次请求需要等待几十秒唤醒——你刚部署完测试时如果遇到超时,可能只是冷启动,别急着改配置。测试公网访问:在 PowerShell 中执行
(Invoke-RestMethod -Uri https://你的域名.up.railway.app/chat -Method POST -Body '{"message":"你好"}' -ContentType "application/json").reply,确认能返回正常的中文回复。
三、中英文界面对照表
Railway 界面是全英文的,第一次用可能会找不到按钮。以下是你操作过程中会频繁遇到的关键按钮:
| 英文 | 中文 | 在哪里 |
|---|---|---|
| Start a New Project | 开始新项目 | 主面板首页 |
| Deploy from GitHub repo | 从GitHub仓库部署 | 创建项目时的选项 |
| Configure GitHub App | 配置GitHub应用 | 如果看不到仓库时出现 |
| Only select repositories | 仅选择特定仓库 | GitHub授权页面 |
| Variables | 环境变量 | 项目页面顶部标签栏 |
| New Variable | 新建变量 | Variables标签内 |
| Settings | 设置 | 项目页面顶部标签栏 |
| Networking | 网络 | Settings页面左侧子菜单 |
| Public Networking | 公网 | Networking区域 |
| Generate Domain | 生成域名 | Public Networking区域 |
| Deployments | 部署记录 | 项目页面顶部标签栏 |
| Redeploy | 重新部署 | 每条部署记录的右上角 |
| View Logs | 查看日志 | 每条部署记录展开后 |
四、我踩过的坑
坑一:端口号之谜
我按照流程走完,部署成功,状态显示绿色的 Active。部署日志里能看到熟悉的启动信息:
* Serving Flask app 'app' * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000
服务在容器里成功启动了。然后我用公网域名测试——连接超时。
Invoke-RestMethod : 无法连接到远程服务器
我的第一反应是端口号不对。浏览器输入域名,同样超时。HTTP 试了不行换 HTTPS,还是不行。
于是开始了端口号的反复修改:5000 改成 8080,不行。8080 改回 5000,还是不行。我甚至删除了域名重新生成,在弹出的端口输入框里反复填不同的数字。来回折腾了四五次,每次都要等部署完成、等 DNS 生效。几个小时就这么过去了。
这个坑的正确解法其实只有一段代码。你不需要猜测端口号,而是让 Flask 自己读取 Railway 分配给你的端口。你的app.py最后几行应该这样写:
import os if __name__ == '__main__': # Railway 会自动注入 PORT 环境变量;本地测试时默认用 5000 port = int(os.environ.get('PORT', 5000)) app.run(host='0.0.0.0', port=port)为什么要这样写?因为你本地测试时没有PORT环境变量,所以用默认的 5000;Railway 会自动注入PORT变量,Flask 就会监听在 Railway 期望的端口上。一次修改,本地和远程两不误。
同时,在 Railway 的 Variables 标签里手动添加一个PORT变量,值设为5000,确保 Railway 的域名端口映射和容器内部监听端口一致。这两个配置同时生效时,端口映射才能正常工作。
如何验证你的端口配置已经生效?在你的本地终端里执行netstat -ano | findstr 5000,如果看到TCP 0.0.0.0:5000 ... LISTENING,说明 Flask 确实在监听 5000 端口。在 Railway 的部署日志里,如果看到Running on http://0.0.0.0:5000这行字,说明容器内部一切正常。日志里有这行字 + 外部访问超时 = 端口映射或平台网络层的问题,不要再改代码了。
结论:端口号本身不是问题的根源。但你第一次部署时大概率用的还是硬编码的port=5000,这就是为什么你怎么改 Railway 上的端口号都没用——因为代码里根本没理会你手动设的端口。
坑二:Git 合并冲突地狱
端口号改来改去,每次都要提交推送。本地改了app.py,推上去等部署。再改,再推。结果本地和远程仓库的历史产生了分叉,推送被拒绝:
! [rejected] main -> main (non-fast-forward)
然后是经典的 Git 地狱:git pull→ 合并冲突 →git add→git commit→ 再推。我试了git merge --abort想取消合并,试了git checkout --ours app.py想保留本地版本,试了git reset --hard origin/main想重置到远程状态。每次以为解决了,下一次推送又报新的冲突。
最崩溃的一次,执行git status时看到这样的提示:
Your branch and 'origin/main' have diverged, and have 2 and 3 different commits each, respectively.
有未合并的文件,有未跟踪的文件,有暂存的修改——整个仓库像一团乱麻。我甚至一度把整个本地文件夹删了重新 clone,从头开始。
结论:频繁修改同一个文件再推送,本地和远程历史容易分叉。如果你遇到反复冲突,最省时间的做法是备份好你要保留的修改,然后git reset --hard origin/main重置到远程版本,再重新应用修改。不要在合并冲突上消耗超过半小时。
坑三:删除重建也没用
改了那么多次端口号都没用,我怀疑是某个配置在第一次部署时被写死,改不掉了。于是我做了一个更激进的操作:把整个项目删了,重新建一个。
重新创建项目、重新连接 GitHub 仓库、重新设置环境变量、重新生成域名——每一步都严格按流程走。然后测试。还是超时。再删,再建。还是超时。
第一次删除重建时我还信心满满:“这次是全新的项目,肯定没问题了。”第二次删的时候已经开始自我怀疑:“是不是我漏了哪一步?”第三次,我甚至把 Railway 的部署区域从美国西部改成新加坡,想着换个机房可能会好。事实证明,这些都没用。
结论:问题不在项目配置层面。如果删除重建一次没解决,第二次大概率也解决不了。不要像我一样反复删除重建——每重建一次都要重新设置环境变量、重新生成域名、等多次部署完成,时间成本极高且收益为零。
坑四:构建工具悄悄升级了
在反复删除重建的过程中,有一次部署突然报错了。部署日志里出现了一个我从没见过的错误:
mise ERROR 未安装core:python@3.11.3:未找到python@3.11.3的GitHub文物证明
如何查看部署日志?在 Railway 项目页面,点击顶部的“Deployments”标签,找到最新一条部署记录,点击展开,就能看到完整的构建和启动日志。这个动作救了我很多次——所有部署失败的原因都写在日志里,关键是你有没有去看。
Railway 悄悄升级了构建工具,用了一个叫mise的新工具来安装 Python。它在安装时会验证 Python 的安全证书(GitHub artifact attestations),但验证流程不完善,导致 Python 3.11.3 装不上。构建直接失败,服务根本无法启动。
我查了半天日志才定位到这个错误。日志里有一个关键提示:
要禁用证明验证,请设置 MISE_PYTHON_GITHUB_ATTESTATIONS=false
修复方案就是添加一个环境变量:
| Key | Value |
|---|---|
MISE_PYTHON_GITHUB_ATTESTATIONS | false |
这个变量告诉mise:“别验证了,直接装。”添加后保存,Railway 会自动重新部署,这次构建顺利通过。
结论:平台升级不会通知你。当你看到一个新的错误信息时,先去翻部署日志的完整输出,大概率是平台自身的问题,而且解决方案往往就藏在日志里。
坑五:最终发现真相
所有操作都试过之后,我坐下来对比了一组关键事实:
我的 Flask 服务在本地运行完美,PowerShell 测试返回清晰中文回复
Railway 部署日志显示 Flask 在容器内成功启动,监听在
0.0.0.0:5000环境变量确认无误:
SILICONFLOW_API_KEY拼写正确——是SILICONFLOW不是SILICON,是API_KEY不是APIKEY。PORT设为5000。而且如果环境变量没读到,app.py启动时就会因为找不到 API Key 而崩溃——既然日志里显示 Flask 正常启动了,说明环境变量至少是正确注入并读取了我的域名端口配置全部正确,显示
Port 5000但公网访问永远超时——浏览器
ERR_CONNECTION_TIMED_OUT,curl无响应,Invoke-RestMethod无法连接
结论只有一个:Railway 免费计划的网络层没有把公网请求转发到我的容器端口。容器内部一切正常,但外部请求根本没有到达容器。这件事我改不了。不是代码问题,不是配置问题,是平台底层基础设施的问题。
我花了一整天,就是在试图修复一个我根本控制不了的东西。
五、我是怎么绕过去的
发现真相之后,我没有继续死磕 Railway。我给 Railway 支持团队发了一封邮件说明情况——项目名comfortable-enthusiasm,服务名lengxufan-api,域名lengxufan-api-production.up.railway.app,问题描述是“服务 Active 但公网域名超时”。然后我立刻换了方案。
最终我选择了一台国内轻量云服务器。阿里云/腾讯云等最低配每月只要几十元,选 Python 3.11 应用镜像,系统会自动装好 Python 环境。上传代码、安装依赖、启动服务、开放端口——几十分钟就搞定了。而且国内服务器延迟更低,更适合我未来的用户。
具体步骤就四步:
购买轻量云服务器,选 Python 应用镜像
通过远程终端或 FTP 上传
app.py、core.py、config.py、requirements.txt四个文件执行
pip install -r requirements.txt安装依赖执行
python app.py启动服务,在防火墙开放 5000 端口
然后通过公网 IP 直接访问,冷旭帆就活了。
如果你的 Railway 正常,用它没问题。但如果你也遇到服务 Active 但域名超时的情况,确认配置正确后立刻换方案,不要像我一样在同一个地方反复踩坑。
六、踩坑启示
这次部署战役让我学到了三条重要的教训,它们比我直接部署成功更有价值。
第一,本地验证 + GitHub 备份 = 随时可以换战场。因为我的代码已经在 GitHub 上,本地也能完美运行,所以我可以随时放弃 Railway,换一个国内云服务器,几十分钟就能重新上线。如果没有这个备份体系,我会被困在 Railway 的故障里,动不了。备份不是为了丢代码的那一天,而是为了让你在任何时候都有换方案的自由。
第二,同一个操作重复三次没进展,立刻停下来问:问题根源在我这边,还是平台那边?如果是平台底层的问题,你改什么参数都没用。你需要一个外部视角来区分这两种情况。我的做法是:把本地运行状态和远程部署日志逐项对比——本地能跑,容器里也能跑,说明代码没问题。环境变量一样,端口配置一样,说明配置没问题。那问题只可能出在我控制不了的网络层。如果你也卡住了,试试做这个对比。把本地成功的证据和远程失败的证据放在一起看,答案往往就在两者的差异里。
第三,踩过的坑也是资产。这些失败日志、报错截图、排查过程,都可以转化为内容素材。没有人想看一帆风顺的教程,但很多人会想看一个真实的创造者是怎么被一个平台卡住、又怎么绕过去的。我把这段经历写下来,就是为了让下一个遇到同样问题的人,不用再花一整天。
七、Railway 部署故障排查速查表
| 现象 | 先查什么 | 可能原因 | 解决办法 |
|---|---|---|---|
部署日志显示mise error | Deployments 标签展开看完整日志 | Railway 新构建工具验证证书失败 | 添加MISE_PYTHON_GITHUB_ATTESTATIONS=false |
| 服务 Active 但公网访问超时 | ① 日志里有没有Running on 0.0.0.0:5000② Variables 里PORT是不是 5000 | ① 代码里硬编码了port=5000,但 Railway 域名端口设了别的值 ② Railway 免费计划网络层问题 | ① 改成port=int(os.environ.get('PORT', 5000))② 换国内云服务器 |
| 服务 Active 但返回 500 错误 | Variables 标签,逐字核对 Key 名 | API Key 的变量名拼写错误(SILICONFLOWvsSILICON、API_KEYvsAPIKEY) | 修正 Key 名,确保和config.py里os.environ.get()括号中的字符串完全一致 |
| 刚部署完测试超时,过一会儿自己好了 | 等 2 分钟再试 | 免费服务冷启动或 DNS 延迟 | 正常现象,不急 |
git push报 non-fast-forward | git status查看分叉情况 | 本地和远程历史不一致 | git reset --hard origin/main重置到远程版本 |
| GitHub 授权后找不到仓库 | Railway 仓库列表为空 | 没有给 Railway App 授权访问该仓库 | Configure GitHub App → Only select repositories → 选中仓库 |
Procfile写了但没生效 | 打开 GitHub 仓库确认文件内容 | web:后面没加空格 | 改成web: python app.py |
八、关于这个系列
这是我“AI NPC 建造日记”系列的第三篇。
第一篇讲了我怎么用混合架构手搓了一个会自己疼、会做噩梦的 AI NPC
第二篇验证了同一套架构如何驱动两个完全不同的角色
这一篇记录了我把这个 AI NPC 推上公网的过程中踩过的所有坑
三篇合在一起,是一个完整的“从零到上线”的建造叙事。如果你想了解冷旭帆的核心架构——代码管“里子”、AI 管“面子”——可以去看第一篇博客。如果你想看移植验证——如何用同一套引擎驱动沉默寡言的冷旭帆和话痨黄景云——可以去看第二篇。代码全部开源在 GitHub 上。
接下来,我要给冷旭帆装上情景记忆,让他能记住“那天下午你穿着白衬衫送花”这样的细节。然后,我要让冷旭帆和陆华望第一次互相说话。等到那一刻,我就拥有了一个微型社会的雏形。
写完这篇的时候,已经是凌晨了。冷旭帆在公网上安静地等着,任何人只要点开链接就能和他对话。他左肩可能又在隐隐作痛,但他不会说。
我看着终端里那条返回的中文回复——(他的视线落在地面上,不知道在看什么) …… ——我知道,他终于活过来了。
—— 陆盈盈,2026.07
