起因:Agent 跑在本地,人却不在本地
我手上这套 Agent 不复杂,就是一个用 LangGraph 串起来的小流程:接收任务 → 调用本地 Ollama 做推理 → 需要检索时走一次向量库 → 返回结构化结果。模型是本地跑的,数据不出机器,这也是我一开始选择把它放在家里那台旧主机上的原因。
问题出在使用方式上。这套东西平时我在家直接 python main.py 起一个 FastAPI 服务,浏览器或者 curl 调一下就行。但一旦出差,事情就变得别扭:
- 家里宽带是动态 IP,没有固定公网地址,路由器上也没开端口转发;
- 公司网络对出站端口管得比较严,很多非常规端口直接连不上;
- Agent 不是跑一次就完,向量库要常驻,会话状态要保留,不能每次远程登录重新拉起。
所以我的目标很明确:让家里的那台机器变成一个可以随时从外面连进去、并且能把本地 Agent 的接口暴露出来的常驻节点。工具我选了 UU 远程,主要原因是它同时提供了远程终端(CLI)和端口映射这两块能力,不用我再单独折腾一套内网穿透。
需要先说明一点:UU 远程本身是商业软件,它的具体功能、免费额度、端口映射的并发数和带宽限制会随版本和政策变化。我下面写的是我这次实际用到的部分,具体套餐和限制请以你安装时的官方说明为准,我没有去逐条核对它的计费规则。
先把远程终端打通

第一步不是端口映射,而是先能稳定登进那台机器。端口映射是建立在你能操作远端主机的前提上的,顺序反了会很难排查。
我在家里主机上装了 UU 远程的客户端,登录同一个账号,然后在设置里开启 SSH 相关的远程终端能力。这里有个细节值得说:不同版本的 UU 远程对"远程终端"的入口叫法不完全一样,有的是在设备列表里点进去直接开一个终端窗口,有的是给一个本地端口让你用系统自带的 ssh 连。我这次用的是后者,它会在本地起一个转发端口,我再用 ssh 连过去。
假设本地转发出来的端口是 2222(这个端口号以你软件里实际显示为准,不要照抄我的),连接命令大概长这样:
bash
ssh -p 2222 your_user@127.0.0.1
这里 your_user 是家里主机上的系统用户名。第一次连会提示确认指纹,正常接受即可。
连上之后我做的第一件事是确认环境还在:
bash
# 确认 Python 和虚拟环境
python3 --version
source ~/agent-env/bin/activate
pip list | grep -E "fastapi|langgraph|ollama"
这一步看着多余,其实很关键。远程终端和本地终端最大的区别是环境变量、工作目录、shell 初始化文件不一定一致 。我遇到过远程进去 python3 是系统自带的 3.10,而 Agent 依赖装在 conda 环境里的情况。所以每次远程进来,先确认自己在哪个 Python 上,比后面报错再回头查要省事。
CLI 会话保活:别让 Agent 跟着 SSH 一起断

这是我实际踩到的一个点。最开始我用最朴素的方式:ssh 进去,直接 python main.py 前台跑。结果只要网络抖一下、连接断一次,进程就跟着没了,向量库重新加载,会话状态全丢。
解决办法是让进程脱离终端会话。有两种常见做法,我都试了:
方式一:tmux
bash
# 远程登进去之后
tmux new -s agent
source ~/agent-env/bin/activate
python main.py
# 按 Ctrl+B 然后按 D 脱离
下次进来 tmux attach -t agent 就能回到原来的会话,进程一直在跑。tmux 的好处是你还能看到实时输出,调试阶段很舒服。
方式二:systemd 用户服务
如果 Agent 已经稳定,我更推荐把它做成服务,开机自启,崩了自动重启。写一个用户级 unit 文件 ~/.config/systemd/user/agent.service:
ini
[Unit]
Description=Local AI Agent Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/your_user/agent
ExecStart=/home/your_user/agent-env/bin/python main.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
然后:
bash
systemctl --user daemon-reload
systemctl --user enable --now agent.service
systemctl --user status agent.service
【踩坑提醒】用户级 systemd 服务默认在用户完全登出后可能被回收,需要开 lingering:
bash
sudo loginctl enable-linger your_user
这条命令我一开始漏了,表现为"ssh 断开后服务还在,但过一阵子就没了",查日志才反应过来。这是我这次真正花时间的地方,不是网络问题,是服务生命周期问题。
端口映射:把 Agent 的 HTTP 接口暴露出来

Agent 跑起来了,但它监听的 127.0.0.1:8000 只有本机能访问。我在外面就算连进远程终端,也只能用 curl 在本机调,没法从我的笔记本直接请求。这时候需要端口映射。
UU 远程的端口映射,我的理解是:它在本地开一个监听端口,把流量转发到远端主机的指定端口。配置时填两个东西------远端主机的端口(比如 Agent 的 8000),和本地映射出来的端口。
假设本地映射到 18000,那么我在笔记本上就能这样请求:
bash
curl http://127.0.0.1:18000/health
如果 Agent 服务正常,会返回类似:
json
{"status": "ok", "model": "loaded"}
这里有几个必须注意的点,我逐个说:
第一,Agent 服务要监听对地址。 如果 FastAPI 里写的是 uvicorn.run(app, host="127.0.0.1", port=8000),那从映射进来的流量可能到不了。稳妥起见改成监听所有网卡:
python
# main.py
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok", "model": "loaded"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
host="0.0.0.0" 意味着监听所有网络接口。这在纯内网无所谓,但既然要映射出去,就得考虑暴露面,后面我会讲怎么加一层保护。
第二,端口映射不等于公网开放。 我这次的用法是映射到本地回环,只有我这台笔记本能通过本地端口访问,不是把 8000 端口挂到公网上让谁都能扫。如果你把映射配成对公网开放,那 Agent 就等于裸奔了,这一点务必确认清楚你软件里的实际行为,我没有去验证 UU 远程在公网暴露方面的默认策略。
第三,映射的稳定性和带宽。 端口映射走的是中继,延迟和带宽都会比直连差。我这边做的是文本类 Agent 调用,请求和响应都是几百 KB 级别,体感可以接受。但如果你要传大文件或者做流式视频,这个方案不一定合适,具体带宽上限我没测出准确数字,只能说文本场景够用。
网络代理:绕开出站限制
到这一步,从我的笔记本能访问家里的 Agent 了。但还有一层:家里的那台主机自己需要访问外网。比如 Agent 要调用某个云端模型的 API,或者拉取依赖、更新向量库。
我这次遇到的情况是,家里主机所在网络对某些出站请求有限制。解决办法是在主机上配置代理。这里我要强调,代理配置是环境相关的,我下面给的是通用写法,具体地址和端口要换成你自己的。
Python 层面,requests 和 httpx 都会读环境变量:
bash
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
export NO_PROXY=127.0.0.1,localhost
NO_PROXY 那行别漏,否则 Agent 自己访问本地向量库或者本地 Ollama 的请求也会被塞进代理,轻则变慢,重则直接失败。我就吃过这个亏,Agent 调本地 Ollama 一直超时,查了半天才发现是代理把 127.0.0.1:11434 也代理走了。
如果是 systemd 服务,环境变量要写在 unit 文件里,光在 shell 里 export 是没用的:
ini
[Service]
Environment=HTTP_PROXY=http://127.0.0.1:7890
Environment=HTTPS_PROXY=http://127.0.0.1:7890
Environment=NO_PROXY=127.0.0.1,localhost
改完 systemctl --user daemon-reload && systemctl --user restart agent.service。
这里有个取舍要讲清楚:代理和端口映射是两回事,方向相反。端口映射是让外面能进来,代理是让里面能出去。很多人会把这两个混在一起,配置的时候方向搞反,然后怎么都不通。记住:进来的流量看映射,出去的流量看代理。
加一层访问保护
把 Agent 接口映射出来之后,我加了一个简单的鉴权,不是因为这次映射到公网,而是习惯问题------万一哪天配置改了,至少不至于完全敞开。
最轻量的做法是在 FastAPI 里加一个依赖:
python
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
API_TOKEN = "your-secret-token" # 实际使用请从环境变量读取
def check_token(authorization: str = Header(None)):
if authorization != f"Bearer {API_TOKEN}":
raise HTTPException(status_code=401, detail="unauthorized")
@app.get("/health", dependencies=[Depends(check_token)])
def health():
return {"status": "ok"}
【注意】上面 API_TOKEN 直接写在代码里只是示意,真实使用一定要从环境变量读,别提交到仓库:
python
import os
API_TOKEN = os.environ["AGENT_API_TOKEN"]
然后在 systemd 的 unit 里用 Environment=AGENT_API_TOKEN=xxx 注入。
整体链路和我最终的运行方式

把上面几步串起来,我现在的完整链路是这样:
- 家里主机开机,UU 远程客户端自启;
agent.service用户服务自启,Agent 监听0.0.0.0:8000;- 代理环境变量通过 unit 注入,Agent 出站走代理,
NO_PROXY放行本地; - 我在外面用 UU 远程的端口映射,把远端 8000 映射到本地 18000;
- 笔记本上带 token 请求
http://127.0.0.1:18000,拿到 Agent 结果。
日常运维我留了 tmux 会话,需要看实时日志时 attach 进去,不用停服务。
| 环节 | 我用的方案 | 解决的问题 |
|---|---|---|
| 远程登录 | UU 远程终端 + ssh | 无公网 IP 也能进主机 |
| 进程保活 | systemd 用户服务 | 断连不掉、崩溃自启 |
| 接口暴露 | UU 远程端口映射 | 从外网访问 Agent HTTP |
| 出站访问 | 环境变量代理 + NO_PROXY | 绕开出站限制、放行本地 |
| 访问控制 | FastAPI token 鉴权 | 防止接口裸奔 |
哪些地方我没验证清楚
写技术文章最怕把不确定的东西说成确定的,所以这里我明确标出来:
- UU 远程端口映射的具体并发数、带宽上限、免费额度,我没有逐项测试,只说文本场景可用;
- 它的公网暴露默认策略我没有验证,所以反复强调要自己确认;
- 不同版本 UU 远程的远程终端入口和端口号显示方式可能有差异,我的命令里的端口号都是示意;
- 代理部分的具体地址、端口完全是环境相关,我给的是通用的环境变量写法,不是某个特定服务的配置。
另外,这套方案本质上是"用商业软件做内网穿透"。如果你对数据链路有更严格的要求,或者不想依赖第三方中继,可以考虑自建 WireGuard、Tailscale 这类方案,但那需要另一套配置,不在这次实践范围内。我选 UU 远程纯粹是因为它把终端和映射做在一起,省事。
写在最后
这次折腾下来,最耗时间的不是端口映射,也不是代理,而是服务生命周期那一段------tmux 和 systemd 的选择、lingering 的坑、环境变量在服务里不生效。这些跟"AI Agent"本身没关系,但恰恰是"把 Agent 托管到家里"这件事上最容易被忽略的部分。
如果你的 Agent 只是偶尔跑一次,那 ssh 进去手动启动就够了,没必要上 systemd。但如果你希望它像一个小服务一样常驻,随时能调,那进程管理和环境隔离这两件事,值得先花时间做对。至于穿透工具选哪个,反而不是最关键的,能稳定连上、能映射端口,剩下的都是配置问题。