我把 AI NPC 部署到公网,被 Railway 逼疯了整整一天
我把 AI NPC 部署到公网被 Railway 逼疯了整整一天我的冷旭帆在本地跑得好好的Flask API 完美返回中文回复。我想把他部署到 Railway 上让任何人都能访问。我以为一个小时就能搞定。结果我从下午折腾到凌晨反复改了无数次端口号、删了四五次域名、甚至把整个项目删了重建——最后发现问题根本不在我的代码上。一、背景一个在本地活得好好的人先交代一下背景。冷旭帆是我手搓的一个 AI NPC——会自己疼、会做噩梦、会在你不在的时候默默擦刀。他的核心架构是“手搓感知系统大模型 API”Python 代码精确控制他的情绪值、状态标签、后台时间和记忆大模型负责根据这些状态说符合人设的话。这个项目从今年 4 月中旬启动经历了从纯规则引擎到混合架构的多次迭代。5 月底完成了四轮测试修复主测试通过率从 18.75% 提升到 93.75%。6 月初我把单文件脚本拆成了标准的三层架构——config配置和人设、core核心逻辑、appFlask 服务——然后本地跑通了 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(host0.0.0.0, portport)为什么要这样写因为你本地测试时没有PORT环境变量所以用默认的 5000Railway 会自动注入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这行字说明容器内部一切正常。日志里有这行字 外部访问超时 端口映射或平台网络层的问题不要再改代码了。结论端口号本身不是问题的根源。但你第一次部署时大概率用的还是硬编码的port5000这就是为什么你怎么改 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 未安装corepython3.11.3未找到python3.11.3的GitHub文物证明如何查看部署日志在 Railway 项目页面点击顶部的“Deployments”标签找到最新一条部署记录点击展开就能看到完整的构建和启动日志。这个动作救了我很多次——所有部署失败的原因都写在日志里关键是你有没有去看。Railway 悄悄升级了构建工具用了一个叫mise的新工具来安装 Python。它在安装时会验证 Python 的安全证书GitHub artifact attestations但验证流程不完善导致 Python 3.11.3 装不上。构建直接失败服务根本无法启动。我查了半天日志才定位到这个错误。日志里有一个关键提示要禁用证明验证请设置 MISE_PYTHON_GITHUB_ATTESTATIONSfalse修复方案就是添加一个环境变量KeyValueMISE_PYTHON_GITHUB_ATTESTATIONSfalse这个变量告诉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_OUTcurl无响应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 errorDeployments 标签展开看完整日志Railway 新构建工具验证证书失败添加MISE_PYTHON_GITHUB_ATTESTATIONSfalse服务 Active 但公网访问超时① 日志里有没有Running on 0.0.0.0:5000② Variables 里PORT是不是 5000① 代码里硬编码了port5000但 Railway 域名端口设了别的值 ② Railway 免费计划网络层问题① 改成portint(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-forwardgit 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