[{"content":"摘要: 基于真实踩坑案例，实现 1Panle 平台 + Gitea 私有仓库快速部署。\n前情提要 前面文章中提到了 MCP server 快速部署案例。\n其中有一种场景：用户自己开发了一个 mcp server 代码，希望运行在 1Panel 的MCP 组件中，而不是只能够在本地运行。\n并且文章中提到，1Panel 中新增的 mcp server配置如下——\n1 /bin/sh -c \u0026#34;apk add git \u0026amp;\u0026amp; uvx --from git+http://lab.nxlan.cn:3008/pdream/jobsearch-mcp-server.git jobsearch-mcp-server --transport stdio\u0026#34; 上面用到代码仓库 http://lab.nxlan.cn:3008/pdream/jobsearch-mcp-server.git 就是本文主角——gitea 个人仓库。\n一、Gitea 基础配置 延续1Panel 中特色功能：图形化部署+应用内部关联。\n可以 2分钟快速实现 gitea 应用的部署，5分钟完成初始化，10分钟完成反向代理配置。\n之后，就可以同步自己的代码到gitea 仓库中了。\n感兴趣的话，不妨参考本文一步一步操作完成。\n1.1 Gitea 应用创建 首先在 \u0026ldquo;应用商店\u0026rdquo; -\u0026gt; \u0026ldquo;全部\u0026rdquo; -\u0026gt; \u0026ldquo;搜索\u0026rdquo; 栏中输入 gitea 关键字，找到可以安装的应用。\n注意点击 第一个 Gitea \u0026ldquo;安装\u0026rdquo;。\n如果 1Panel 中没有安装过数据库服务，会有需要安装数据库的提醒（如下图）——\n如果是这种情况，就需要先安装数据库。\n点击\u0026quot;去安装\u0026quot;后，直接跳转到数据库安装界面——\n点击**\u0026ldquo;确认\u0026rdquo;** 按键，等待数据库安装完成。\n如果点击\u0026quot;安装\u0026quot; 后，提示：\u0026ldquo;未开启外部映射，将无法从外外网访问数据库服务\u0026rdquo;。\n可以根据自己需求，再勾选 \u0026ldquo;高级设置\u0026quot;中的 \u0026ldquo;端口外部访问\u0026quot;选项。\n一般不建议 直接开放数据库服务到外部，所以我这里默认不启用此功能。\n再次回到Gitea 安装界面，数据库服务可以选择刚创建好的postgres 了。\n因为，稍后使用 ssh 协议同步代码，需要访问 gitea 的ssh 服务，这里需要勾选 \u0026ldquo;端口外部访问\u0026rdquo; 的选项。\n点击\u0026quot;确认\u0026quot;后，就开始自动安装 gitea 应用了。\n安装成功后，可以在应用商店中看到刚新增的两个应用——\n1.2 追加反代配置 默认安装完成时，gitea 应用的web 服务绑定在宿主机的 3000端口。对应我的主机就是 http://lab.nxlan.cn:3000。\n如果1Panel 在你的内部局域网，就可以直接访问上述页面，进行后续初始化工作。（可以跳过本段）\n但是，如果gitea 宿主机在云上，又怎么能直接访问这个管理页面呢？\n一种方式就是：追加反向代理配置，通过已经上线的https 服务，代理/gitea 流量到 http://127.0.0.1:3000 的真实服务。\n还有一种方法是：登录到云主机 管理页面，在安全策略下，追加一条——放行公网访问主机 3000 端口的策略。\n考虑到：这样临时放行后，还是要取消并开通反代，我就没有这么操作。\n操作步骤上，还是先进入**\u0026ldquo;网站\u0026rdquo; -\u0026gt; \u0026ldquo;反向代理\u0026rdquo; -\u0026gt; \u0026ldquo;创建\u0026rdquo;**， 创建一条新代理规则，请求路径假设就是 /gitea。\n并在原始代理配置文件内容基础上追加——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 location ^~ /gitea { # 追加配置：去掉 /gitea 前缀，发给后端 rewrite ^/gitea(/.*)$ $1 break; # --- 默认设置 --- proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_http_version 1.1; add_header X-Cache $upstream_cache_status; proxy_ssl_server_name off; proxy_ssl_name $proxy_host; } 保存反代配置后，进入gitea 配置文件目录——/opt/1panel/apps/gitea/gitea/data/gitea/conf\n修改配置文件app.ini中的一条配置：\n1 2 3 4 5 6 7 8 9 10 11 [server] APP_DATA_PATH = /data/gitea DOMAIN = localhost SSH_DOMAIN = localhost HTTP_PORT = 3000 # 修改为自己外部域名和代理接口 ROOT_URL = https://lab.nxlan.cn/gitea/ DISABLE_SSH = false SSH_PORT = 22 SSH_LISTEN_PORT = 22 LFS_START_SERVER = false 修改好后，重启gitea 应用新配置。再次访问反代地址—— https://lab.nxlan.cn/gitea，就可以看到初始化配置页面了。\n1.3 完成应用初始化 在上述初始化页面中，1Panel 已经帮我们补充好了数据库地址、名称、用户、密码等信息。\n这里保持默认，点击 \u0026ldquo;立即安装\u0026rdquo; 完成最后的初始化工作。\n最后，完成管理员账户的设置——\n管理账户创建后，重新登录。\n1.4 同步代码至仓库 同步本地写好的代码前，需要先在 Gitea 上创建仓库。\n我们的第一个代码仓库，名字就叫 \u0026ldquo;jobsearch-mcp-server\u0026rdquo;。\n为了方便mcp server 拉取项目代码，项目可见性不勾选 \u0026ldquo;将仓库设为私有\u0026rdquo;——也就是公开的意思。\n其他保持默认，这样我们第一个代码仓库就创建完成了。\n仓库创建完后，需要追加个人应用 访问令牌——方便开发主机同步代码。\n在 \u0026ldquo;个人信息\u0026rdquo; -\u0026gt; \u0026ldquo;应用\u0026rdquo; -\u0026gt; \u0026ldquo;生成新的令牌\u0026rdquo; 中 输入令牌名称，并追加repository 等读写的权限。\n添加成功后，会出现一行令牌字符串，类似于——a385cac305f80ff43dfe202d727b5a33176d18b3\n在开发主机上，按照以下命令顺序执行，同步代码到gitea 仓库：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # git 项目初始化 git init # 查看当前 remote git remote -v # 删除过往 remote git remote remove origin # 追加 git remote add origin https://pdream:[TOKEN]@lab.nxlan.cn/gitea/pdream/jobsearch-mcp-server.git # 推送本地代码到仓库 git push --set-upstream origin master Enumerating objects: 52, done. Counting objects: 100% (52/52), done. Delta compression using up to 4 threads Compressing objects: 100% (42/42), done. Writing objects: 100% (52/52), 100.22 KiB | 5.90 MiB/s, done. Total 52 (delta 19), reused 0 (delta 0), pack-reused 0 remote: . Processing 1 references remote: Processed 1 references in total To https://lab.nxlan.cn/gitea/pdream/jobsearch-mcp-server.git * [new branch] master -\u0026gt; master branch \u0026#39;master\u0026#39; set up to track \u0026#39;origin/master\u0026#39;. 提示同步成功。此时访问仓库页面，就可以看到刚上传的代码了。\n二、1Panel MCP Server 部署（从 Gitea 拉取） 2.1 场景 在 1Panel 上通过 MCP 管理界面部署 MCP Server时，因为源代码在个人 Gitea上，所以在MCP server 启动要指定git项目地址。\n2.2 正确启动命令 经过反代服务器（推荐）：\n1 /bin/sh -c \u0026#34;apk add git \u0026amp;\u0026amp; uvx --from git+https://lab.nxlan.cn/gitea/pdream/jobsearch-mcp-server jobsearch-mcp-server --transport stdio\u0026#34; 直连 Gitea（不经反代，数据未加密）：\n1 /bin/sh -c \u0026#34;apk add git \u0026amp;\u0026amp; uvx --from git+http://lab.nxlan.cn:3000/pdream/jobsearch-mcp-server.git jobsearch-mcp-server --transport stdio\u0026#34; 参数说明：\n--transport stdio — 1Panel MCP 托管必须使用 stdio 传输模式（解决 SSE 嵌套冲突） 2.3 注意事项 路径格式：git+https:// 而非本地路径，容器内无法访问宿主机文件系统\n传输模式：必须加 --transport stdio，否则与 1Panel MCP 托管机制冲突\n反代 vs 直连：经反代走 443 端口（SSL 由反代终止），直连走 3000 数据不经加密传输（服务直接暴露在公网）。\n环境变量：在jobserch 这个 MCP Server 项目中需要声明环境变量 \u0026ldquo;DEEPSEEK_API_KEY\u0026rdquo;，否则提示 OPENAI_API_KEY 未配置。\n三、补充 ：使用ssh方式同步代码 前提：\nGitea 应用创建时，勾选了 \u0026ldquo;端口外部访问\u0026rdquo; 的选项。\n3.1 SSH Key 生成 开发主机密钥生成：\n1 2 3 4 ssh-keygen -t ed25519 -C \u0026#34;your_email@xx.com\u0026#34; # 输出： # id_ed25519 (私钥) # id_ed25519.pub (公钥) 添加主机的ssh key到 Gitea：\n1 2 3 4 5 6 7 # 查看公钥 cat ~/.ssh/id_ed25519.pub # 复制内容 → Gitea Web UI → 用户设置 → SSH 密钥 → 添加 # 验证连通性 ssh -p 222 git@lab.nxlan.cn -T # 预期：Welcome to Gitea! 仓库 ssh 密钥截图：\n3.2 修改git remote 配置 1 2 3 4 5 6 7 8 # 删除之前的 remote git remote remove origin # 将 之前配置的 HTTPS 改为 SSH（注意端口 222） git remote set-url origin ssh://git@lab.nxlan.cn:222/pdream/jobsearch-mcp-server.git # 验证 git remote -v 3.3 HTTP 与 SSH 的分工 通道 用途 使用场景 HTTP（S） 拉取源代码 MCP Server 容器内 uvx --from git+https://... 从 Gitea 拉代码 HTTP（S） 上传源代码 本地编辑完成后 git push 将代码同步回 Gitea。此时origin为： https://pdream:[TOKEN]@lab.nxlan.cn/gitea/pdream/jobsearch-mcp-server.git SSH 上传源代码 本地编辑完成后 git push 将代码同步回 Gitea 。此时origin为： ssh://git@lab.nxlan.cn:222/pdream/jobsearch-mcp-server.git 本例实战：\nMCP Server 部署：容器通过 HTTPS 拉取 jobsearch-mcp-server 源码 代码编辑后推送：本地也通过 HTTPS 方式 ( https://pdream:[TOKEN]@lab.nxlan.cn/...) 提交到 Gitea 两者各司其职，不冲突。\n四、mcp server 排错速查 症状 原因 解法 uvx --from /path/to/repo 报错 容器内无法访问宿主机路径 改用 git+https:// git clone 卡住/超时 容器网络不通或缺 git apk add git，检查网络 SSL certificate problem 自签证书 git config --global http.sslVerify false 容器启动后立刻退出 缺环境变量 检查 OPENAI_API_KEY 等必填变量 SSH 克隆认证失败 端口非 22 确认 Gitea SSH 端口为 222 五、Principle（经验规则） supergateway 容器内操作默认缺 git，先用 apk add git 兜底 MCP server 启动命令只支持单字段，用 /bin/sh -c 串联多条指令 使用 HTTPS 服务推送源代码时，注意令牌权限要给够，否则会报告 403 的错误 Gitea SSH 端口映射为 222，配置 remote 时需注意指定端口 ","date":"2026-04-22T16:03:12+08:00","image":"https://r2.blog.nxlan.cn/PicGobuild_gitea_title.png","permalink":"https://blog.cba.nxlan.cn/p/gitea/","title":"Gitea 私有代码仓库快速部署"},{"content":"前情提要 之前发布的文章：“手把手带你薅 Gemini 付费 API 与 Google 免费服务器” 被人投诉下架了。\n这里把文章链接放出来，感兴趣的同学可以去薅一下——\n前提：需要先开通自己帐号的 google pro服务\n详细步骤：https://blog.cba.nxlan.cn/p/setup_gemini_api\n具体怎么开通、初始化机器、登录的过程，上面文章中已经说得很清楚了。\n只是还剩下一个任务，也就是本文的主题：\n使用1Panel 申请https 免费证书，搭建MCP server 供Agent使用。\n肝了两天，文章有点长，觉得好用点个赞呗。\n步骤概要 强化防火墙策略 ddns-go 组件安装 1panel 安全配置优化 (启用ACME证书注册 ) 启用 MCP server 反向代理服务配置 可以一键安装“龙虾” ^.^ （也是我推荐的方式） Step0 强化防火墙策略 系统初始化后。VPC上 默认放行了 ssh http https 这些服务端口。\n具体到OS层面，熟悉Linux 的同学知道，默认INPUT 方向的策略是全部放行的——\n1 2 3 \u0026gt; iptables -nvL INPUT Chain INPUT (policy ACCEPT 85401 packets, 12M bytes) pkts bytes target prot opt in out source destination 这就导致一个什么问题呢？\nssh 服务经常被暴力攻击——\n所以，系统初始化后，第一步是更新系统组件： apt update \u0026amp; apt upgrade 。\n第二步就应该是 加固ssh 服务——避免被外部暴力破解。\n这里，使用了iptables 的 \u0026ldquo;recent\u0026quot;模块，去动态创建\u0026amp;更新 一个叫做\u0026quot;openssh\u0026quot;的白名单库。\n实现只有白名单库里的公网IP 可以访问这台 Ubuntu的ssh服务，进一步说：只有这些白名单地址所在的地址\n1. 创建iptabls 策略脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026gt; cat /usr/local/sbin/Protect_SSH_port.sh #!/bin/bash # # set iptables input rules for protecting ssh port service # This script is intended to be run by root via a systemd service. # Flush ONLY the INPUT chain rules to ensure a clean state before applying new ones. /sbin/iptables -F INPUT # Set rules /sbin/iptables -I INPUT 1 -p tcp -m state --state RELATED,ESTABLISHED -j ACCEPT # option： 放行Google IDC的跳板登录 /sbin/iptables -I INPUT 2 -s 34.81.160.0/24 -p tcp --dport 22 -j ACCEPT /sbin/iptables -I INPUT 3 -p icmp --icmp-type 8 -m length --length 879 -j LOG --log-prefix \u0026#39;SSH_OPEN_KEY\u0026#39; /sbin/iptables -I INPUT 4 -p icmp --icmp-type 8 -m length --length 879 -m recent --name openssh --set --rsource -j ACCEPT /sbin/iptables -I INPUT 5 -p tcp --dport 22 --syn -m recent --name openssh --rcheck --seconds 60 --rsource -j ACCEPT /sbin/iptables -I INPUT 6 -p tcp --dport 22 -j DROP /sbin/iptables -A INPUT -p icmp -j DROP 2. 创建 protect-ssh 服务，设置开机运行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026gt; cat /etc/systemd/system/protect-ssh.service [Unit] Description=Apply custom iptables rules to protect SSH port After=network.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/sbin/Protect_SSH_port.sh # 当服务停止时，只清空 INPUT 链的规则 ExecStop=/sbin/iptables -F INPUT [Install] WantedBy=multi-user.target 启动并查看服务状态——\n1 2 3 4 5 6 7 8 9 \u0026gt; systemctl enable protect-ssh \u0026gt; systemctl restart protect-ssh \u0026gt; systemctl status protect-ssh ● protect-ssh.service - Apply custom iptables rules to protect SSH port Loaded: loaded (/etc/systemd/system/protect-ssh.service; enabled; preset: enabled) Active: active (exited) since Thu 2026-03-09 16:43:15 CST; 7s ago Process: 141811 ExecStart=/usr/local/sbin/Protect_SSH_port.sh (code=exited, status=0/SUCCESS) Main PID: 141811 (code=exited, status=0/SUCCESS) CPU: 42ms 3. 效果展示 陌生用户，默认没有登录令牌。\n连ssh 会话都建立不起来——\n知道登录令牌，先“敲门”登记一下——\n这里有个计算公式：\n因为： ICMP令牌长度 = Packetsize + ICMP 头部 [ 8 字节 ] + IP 头部 [ 20 字节 ]\n所以：packetsize [ 敲门令牌 ] = ICMP令牌长度 [ 这里是879 ] - 8 - 20 = 851 字节\nping通主机后，才可以正常登录——\n这样就可以避免：主机被陌生人/主机 探测、破解的可能。\n部署后，再也没有陌生人的登录记录。\nStep1 ddns-go 组件安装 因为云主机的固定IP是收费的，为了减少支出，在云主机创建时外部IP是临时的。\n外部IP地址不固定就带来一个问题——某天google 给主机更换外部IP 后，域名访问就失效了。\n除非人工重新进入 GCP 控制页面，查看新的IP，再手动重新映射域名。\n有没有简单的办法解决这个问题呢？有的，兄弟，有的！\n解决办法就是常听到的ddns 服务——通过一个探测脚本，定时去更新域名和IP映射关系。\n所以，下面以ddns-go 组件为例，在这台ubuntu 上完成域名自动映射服务。\n1. 下载ddns-go 源码，创建应用目录 先下载源码——\n1 2 3 4 5 6 \u0026gt; cd /tmp/ wget https://github.com/jeessy2/ddns-go/releases/download/v6.16.5/ddns-go_6.16.5_linux_x86_64.tar.gz tar -zxvf ddns-go_6.16.5_linux_x86_64.tar.gz \u0026gt; /tmp# ./ddns-go -v v6.16.5 创建应用目录和空的配置文件——\n1 2 3 mkdir -p /usr/local/bin/ddns-go touch /usr/local/bin/ddns-go/ddns_go_config.yaml cp /tmp/ddns-go /usr/local/bin/ddns-go/ 2. 导入配置文件 ddns-go配置文件模板如下。\n注意更新 ddns账户的域名和 TOKEN（这里以CloudFlare 上的DNS 服务为例）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 # cat /usr/local/bin/ddns-go/ddns_go_config.yaml dnsconf: - name: cloudflare ipv4: enable: true gettype: url url: https://checkip.synology.com, https://ddns.oray.com/checkip, https://ip.3322.net, netinterface: ens4 cmd: \u0026#34;\u0026#34; domains: - YOUR-DOMAIN-NAME ipv6: enable: false gettype: url url: https://speed.neu6.edu.cn/getIP.php, https://v6.ident.me, https://6.ipw.cn netinterface: \u0026#34;\u0026#34; cmd: \u0026#34;\u0026#34; ipv6reg: \u0026#34;\u0026#34; domains: - \u0026#34;\u0026#34; dns: name: cloudflare id: \u0026#34;\u0026#34; secret: YOUR-DDNS-TOKEN extparam: \u0026#34;\u0026#34; ttl: \u0026#34;\u0026#34; user: username: YOURNAME password: YOURPASS webhook: webhookurl: YOUR-FEISHU-WEBHOOK webhookrequestbody: |- { \u0026#34;msg_type\u0026#34;: \u0026#34;post\u0026#34;, \u0026#34;content\u0026#34;: { \u0026#34;post\u0026#34;: { \u0026#34;zh_cn\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;您的谷歌云主机公网IP变了\u0026#34;, \u0026#34;content\u0026#34;: [ [ { \u0026#34;tag\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;新的IPv4地址：#{ipv4Addr}\u0026#34; } ], [ { \u0026#34;tag\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;域名更新结果：#{ipv4Result}\u0026#34; } ] ] } } } } webhookheaders: \u0026#39;Content-Type: application/json\u0026#39; notallowwanaccess: false lang: zh 3. 创建ddns-go 服务，设置开机运行 和之前一样，为ddns-go 应用创建一个系统服务。\n路径和上面一致的话，复制粘贴就好。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # cat /etc/systemd/system/ddns-go.service [Unit] Description=Simple and easy to use DDNS. Automatically update domain name resolution to public IP (Support Aliyun, Tencent Cloud, Dnspod, Cloudflare, Callback, Huawei Cloud, Baidu Cloud, Porkbun, GoDaddy...) ConditionFileIsExecutable=/usr/local/bin/ddns-go/ddns-go Requires=network.target After=network-online.target [Service] StartLimitInterval=5 StartLimitBurst=10 ExecStart=/usr/local/bin/ddns-go/ddns-go \u0026#34;-l\u0026#34; \u0026#34;127.0.0.1:9876\u0026#34; \u0026#34;-f\u0026#34; \u0026#34;300\u0026#34; \u0026#34;-cacheTimes\u0026#34; \u0026#34;5\u0026#34; \u0026#34;-c\u0026#34; \u0026#34;/usr/local/bin/ddns-go/ddns_go_config.yaml\u0026#34; Restart=always RestartSec=120 [Install] WantedBy=multi-user.target 然后，启动服务——\n1 2 3 \u0026gt; systemctl enable ddns-go \u0026gt; systemctl start ddns-go \u0026gt; systemctl status ddns-go 可以看到类似日志： Apr 1 11:47:22 systemd[1]: Started ddns-go.service - Simple and easy to use DDNS. Automatically update domain name resol\u0026gt; Apr 1 11:47:22 ddns-go[242234]: 2026/04/15 11:47:22 监听 127.0.0.1:9876 Apr 1 11:47:22 ddns-go[242234]: 2026/04/15 11:47:22 你的IP 35.212.158.199 没有变化, 域名 cloud.nxlan.cn\n4. 效果展示 当云主机 IP 地址变化，会通知到feishu 中。\n因为，目前ddns-go的web 管理页面只开放在本地的 127.0.0.1:9876上，目前还不能通过web 方式去配置、修改它。\n等到 Step4 中的https 反向代理服务配置好后，就可以通过web 界面去管理了。（服务支持的 DDNS 厂商还是很多的，根据自己情况选择吧）\nStep2 1panel 安全配置优化 1. 加固ssh 登录 1Panel的一个好处是：通过web 界面图形化地查看系统日志、设置系统基础配置。\n例如，这里的ssh 服务。\n进入**\u0026ldquo;系统\u0026rdquo; -\u0026gt; \u0026ldquo;SSH 管理\u0026rdquo;**页面。\n关闭密码认证和反向解析。并且设置root用户“ 仅允许密钥登录 ”。\n如果对Linux系统比较熟悉，建议将root 用户的登录也关闭掉。\n登录时使用 google 创建云主机时自动创建的普通用户帐号和密钥就行。\n此时只需将密钥公钥信息添加到家目录的authorized_keys文件中就行。\n~# cat /home/YOURUSER/.ssh/authorized_keys ecdsa-sha2-nistp256 AAAA\u0026hellip;..\n然后，点击页面中的“密钥信息”，1Panel会自动创建一组登录 密钥用于root 用户登录。\n进一步，点击“详情”。把公钥和私钥信息全部“下载”下来。用于本地 ssh 客户端登录这台Google Cloud主机。\n点击“授权密钥”，可以看到密钥信息已经加载到 root 用户的ssh 白名单（就是 /root/.ssh/authorized_keys）中了。\n配合上一步的ddns 动态域名，使用ssh 客户端就可以顺利以root 用户身份登录 这台云主机了。\n2. 申请 ACME 证书 其实也就是申请 \u0026ldquo;Let\u0026rsquo;s Encrypt\u0026rdquo; 颁发的免费证书，并自动续签。\n想必你也不希望 自己的数据“裸奔”在不可信的互联网上。\n首先来到**\u0026ldquo;网站\u0026rdquo; -\u0026gt; \u0026ldquo;证书\u0026rdquo;** 页面。\n点击“DNS 账户”，关联自己的 DNS 域名和TOKEN。\n“类型” 这里与之前ddns-go 域名设置中的域名服务商保持一致——CloudFlare。\n点击 “ACME 账户”，创建一个Let\u0026rsquo;s Encrypt账户，邮箱输入自己常用的就行。\n最后，点击\u0026quot;申请证书\u0026rdquo;，完成自动化证书申请和更新的配置。\n注意，\u0026ldquo;验证方式\u0026quot;为刚才创建的DNS 帐号，并勾选\u0026quot;自动续签\u0026rdquo;。这样免费证书到期后，也不用人工干预，就可以自动续续订证书 。\n申请过程需要等1分钟左右，日志可以查看进度和完成情况——\n拿到证书，后续就可以用在很多地方，例如下面的 1Panel 管理页面。\n3. 1panel 管理页面加固 来到 \u0026ldquo;面板设置\u0026rdquo; -\u0026gt; \u0026ldquo;安全\u0026rdquo; 页面。\n绑定自己域名，并开启 \u0026ldquo;面板 SSL\u0026rdquo;。开启后，1Panel 管理页面会关联之前申请到的 ACME 证书。（如下图中的效果）\n同时最好开启两步验证，进一步加强登录安全。\n开启两步验证后的效果——\nStep3 启用 MCP server 1. MCP server 初始配置 首先来到 \u0026ldquo;AI\u0026rdquo; -\u0026gt; \u0026ldquo;MCP\u0026rdquo; -\u0026gt; \u0026ldquo;Servers\u0026rdquo; 。\n我们知道，MCP 下层用的是HTTP 协议，为了不直接将MCP server 暴露在外网，需要先安装 HTTP 反向代理服务。\n进入\u0026quot;应用商店\u0026quot; -\u0026gt; 筛选\u0026quot;web 服务器\u0026quot;标签，安装 OpenResty。\n该应用和后面提到的每一个独立的 MCP server，本质上都是以docker 方式运行的运行时。\n关于OpenResty 的简单介绍——\n2. 绑定网站 选中 \u0026ldquo;MCP\u0026quot;页面中的\u0026quot;绑定网站\u0026rdquo;——\n“域名”还是输入 ddns-go 那用到的自己域名。\n再次关联之前通过 ACME 拿到的证书。\n相同的证书，用在不同的服务上：\n之前是用于1Panel管理页面的登录（非443端口），这次是用于标准443 端口上全部https服务的反向代理。\n反代类似于在客户端和真实服务间加了一层垫片 。它的好处是：\n缩小被攻击的范围；2. 隔离不同服务；3. 内部服务不用重复申请证书；4. 统一访问日志，可追加安全控制策略。 3. 创建第一个MCP 服务 我们先创建一个常用的MCP 服务——sequential-thinking。\n它可以为 大模型对话，提供多轮思考的上下文内容。\n\u0026ldquo;类型\u0026ldquo;选择 npx。 \u0026ldquo;启动命令\u0026ldquo;中填入——\n1 npx -y @modelcontextprotocol/server-sequential-thinking 如果不使用HTTP 反向代理服务，这里就应该允许\u0026quot;端口外部访问\u0026rdquo;。\n因为之前已经启用了 OpenResty ，访问这个 MCP server 的请求，都会经反向代理转给真实服务。\n这里真实服务端口（8002）自然也不用暴露到外网。\n上面\u0026quot;SSE 路径\u0026quot;就是 MCP server 的服务 路径。\n来到 \u0026ldquo;网站\u0026rdquo; -\u0026gt; \u0026ldquo;HTTPS 域名\u0026rdquo; -\u0026gt; \u0026ldquo;反向代理\u0026rdquo; 中可以看到，1Panel 自动为MCP 服务追加了 一条反向代理配置。（下图第二条）\n此时，测试一下这个MCP server 地址——\n服务正常响应了。\n\u0026ldquo;网站日志\u0026quot;中 也可以看到访问记录。\n怎么样是不是很简单？分分钟一个 MCP server 服务就启动和上线了。\n后续文章中，也会提到这个 thinking 工具的使用案例。\n4. 运行自己的MCP 源码 上面的方式，本质是把他人写好并打包发布 的 MCP server代码拉取到 1Panel 的supergateway 容器中运行。\n那能不能把自己写好的服务，也跑在1Panel 上呢？例如之前文章 MCP 案例： 求职助手 中的代码。\n这就相当于有一个 7＊24 的服务跑在云主机上，不用每次运行MCP server时 输入那一长串启动命令——\n1 uv --directory S:\\trae_pj\\jobsearch_mcp_server-1.0.0\\src\\jobsearch_mcp_server run jobsearch-mcp-server 说干就干。\n测试下，来可以通过 git 拉取项目源代码到1Panel 的supergateway 容器中，并以 uvx 方式更新并运行我们自己的代码 。\n效果如下。\n1Panel 配置方面不复杂，一页图就能看明白——\n\u0026ldquo;类型\u0026rdquo; 选择uvx，\u0026ldquo;启动命令\u0026quot;就这么一行。\n1 /bin/sh -c \u0026#34;apk add git \u0026amp;\u0026amp; uvx --from git+http://lab.nxlan.cn:3008/pdream/jobsearch-mcp-server.git jobsearch-mcp-server --transport stdio\u0026#34; 这里使用了 gitea 的 docker版本，用于管理项目代码。它可以兼容 git 命令。\n并且页面也类似于 github 的效果，属于轻量化的本地部署方案，回头有机会再讲讲这块的搭建和设置。\n为什么说 uvx 可以刷新最新代码呢？看下日志就会发现，它每次重启都会从gitea 上同步下源码。\n这样每次改好源码，在\u0026quot;AI\u0026rdquo; -\u0026gt; \u0026ldquo;MCP\u0026rdquo; -\u0026gt; \u0026ldquo;Servers\u0026rdquo; 下重启jobsearch 这个应用，就自动完成了更新和发布。\nStep4 反向代理服务配置 1. 自动创建的反代服务 在 \u0026ldquo;网站\u0026rdquo; -\u0026gt; \u0026ldquo;HTTPS 域名\u0026rdquo; -\u0026gt; \u0026ldquo;反向代理\u0026rdquo; 中可以看到：\n之前启用mcp server 时，1Panel 自动帮我们创建好的两个反向代理配置。\n具体配置长啥样呢？点击 \u0026ldquo;源文\u0026rdquo; 就可以看到详细配置。这里以 jobsearch 这个服务为例——\n1 2 3 4 5 6 7 location ^~ /jobsearch { proxy_pass http://127.0.0.1:8005/jobsearch; proxy_buffering off; proxy_http_version 1.1; proxy_set_header Connection \u0026#39;\u0026#39;; chunked_transfer_encoding off; } 简单地说就是，当客户端浏览器访问站点 (https://lab.nxlan.cn) 时，反向代理服务会根据客户端请求的路径进行请求转发。例如这里: 当匹配客户请求含有 \u0026ldquo;/jobsearch\u0026rdquo; 就转发至 \u0026ldquo;http://127.0.0.1:8005/jobsearch\u0026rdquo;。\n同理，当匹配客户请求含有 \u0026ldquo;/sequential-thinking\u0026rdquo; 就转发至 \u0026ldquo;http://127.0.0.1:8000/sequential-thinking\u0026rdquo;。\n1Panel自动创建的代理设置是简单场景下的配置，没问题就不用手动修改。\n可是，还有些特殊的web 应用，就不能使用默认的配置。\n2. ddns-go 的反代配置 本文涉及需要手动修改的配置，有两个——\n一个是前面安装的 ddns-go, 它也是有web 管理界面的 。 还有一个就是 OpenClaw 小龙虾了。 先看ddns-go 的——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 location ^~ /ddns { # 1. 请求时：去掉 /ddns 前缀，发给后端 rewrite ^/ddns(/.*)$ $1 break; # 2. 响应时：如果后端想重定向到 /，就把它修正为 /ddns/ # 这条规则能修正所有类似 /login -\u0026gt; /ddns/login 的重定向 proxy_redirect / /ddns/; # --- 其他代理设置 --- proxy_pass http://127.0.0.1:9876; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control no-cache; proxy_ssl_server_name off; } 如果在原始服务中写死了请求 url ，所以当用户发起这种请求到达代理服务时，会破坏默认的转发规则。\n此时，需要改写用户请求。——ddns-go 就是这样的例子。\n正常的请求：\n用户请求网页 https://lab.nxlan.cn/ddns \u0026ndash;\u0026gt; 反代 \u0026ndash;\u0026gt;原始服务 http://127.0.0.1:9876\n异常的请求：（因为 ddns-go 的登录页面 路径为 /login）\n用户请求网页 https://lab.nxlan.cn/login \u0026ndash;\u0026gt; 反代 \u0026ndash;\u0026gt; ？？？ 这是什么玩意 我这没有这个规则\n修正规则后的请求：\n用户请求网页 https://lab.nxlan.cn/login \u0026ndash;\u0026gt; 反代 \u0026ndash;\u0026gt; 规则重定向到 https://lab.nxlan.cn/ddns/login \u0026ndash;\u0026gt; 反代发现匹配现有/ddns 路由的规则 \u0026ndash;\u0026gt; 原始服务 http://127.0.0.1:9876/login\n策略应用后，客户端看到的页面效果就是——\n3. OpenClaw 的反代配置 OpenClaw 则是另一种情况：\n从安全性的角度考量，建议开启gateway 中的 \u0026ldquo;allowedOrigins\u0026rdquo; 校验。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026#34;gateway\u0026#34;: { \u0026#34;port\u0026#34;: 18789, \u0026#34;mode\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;bind\u0026#34;: \u0026#34;lan\u0026#34;, \u0026#34;controlUi\u0026#34;: { \u0026#34;allowedOrigins\u0026#34;: [ \u0026#34;https://lab.nxlan.cn\u0026#34;, \u0026#34;http://127.0.0.1:18789\u0026#34;, \u0026#34;http://localhost:18789\u0026#34; ], \u0026#34;dangerouslyAllowHostHeaderOriginFallback\u0026#34;: false, \u0026#34;dangerouslyDisableDeviceAuth\u0026#34;: false }, \u0026#34;auth\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;token\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;${OPENCLAW_GATEWAY_TOKEN}\u0026#34;, } } 该校验内容来自哪里呢？\n需要在代理服务器上追加这部分请求信息，由\u0026quot;X-Forwarded-Proto\u0026quot;和 \u0026ldquo;X-Forwarded-For\u0026rdquo; 字段组成。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 location ^~ / { proxy_pass http://127.0.0.1:18789; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrade\u0026#34;; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600; proxy_send_timeout 3600; proxy_ssl_server_name off; proxy_ssl_name $proxy_host; } 此外还需要追加 OpenClaw 用到的 WebSocket 的支持。\n后续，其他应用，也可以参考这两种方式灵活调整。毕竟，谁还没有一两个 AI 助手呢？\nStep5 一键安装“龙虾” 龙虾最近不是很火么，恰好最近1Panel 丰富了龙虾安装配置，这里简单提一下。\n1. 管理模型 \u0026ldquo;AI\u0026rdquo; -\u0026gt; \u0026ldquo;模型\u0026rdquo; -\u0026gt; \u0026ldquo;模型帐号\u0026rdquo; 中关联自己的 大模型API 帐号。\n2. 创建配置 基础配置 图形化 点击就可以快速完成。\n3. 下载官方通信插件 图形化功能适配，还比较方便，这里以飞书为例。\n当然，复杂的配置还是得修改 openclaw.json 这个主配置文件。\n4. 血泪史 OpenClaw的配置文件是核心，但是它的接口适配做得很难用。\n每一项优化起来太麻烦了——\n目前最让我记忆深刻的有两点：\n我是在使用小龙虾 两个星期后，才知道 feishu 通信渠道插件有两个（一个是 openclaw 社区的，一个是feishu 自己的）；\n每次 OpenClaw 版本大更新后 配置文件的格式就变得乱七八糟，有的配置之前和现在完全不一样——毫无友好性。\n如果为了修复 升级后的配置问题，大概要花上半天时间 继续调试它。\n目前 1Panel 官方维护 一套他们根据 OpenClaw 发布后自动打包的 docker 镜像。\nhttps://hub.docker.com/r/1panel/openclaw\n所以，有需要的话 还是找成功的案例 去复制一下吧，自己折腾还是太折磨了。\n最后，祝AI 时代每个人不用更焦虑，做自己的主人，而不是交给AI 做主。\n","date":"2026-04-08T11:13:01+08:00","image":"https://r2.blog.nxlan.cn/PicGoGenerated_Image_v9wav5v9wav5v9wa.png","permalink":"https://blog.cba.nxlan.cn/p/setup_mcp_server/","title":"手把手初始化GCP云主机并快速完成MCP Server的配置"},{"content":"声明： 本文为真实操作记录，旨在为运维同学在面对“数据误删”或“虚机崩溃”时提供一套经过验证的恢复路径。\n核心金律：操作千万条，备份第一条；挂载新签名，数据不破坏。\n一、 场景回溯：灾难发生的第 0 小时 最近使用1panel(v2版本)时遇到个坑：如果“网站” 创建时指定了app 应用，在删除“网站”时 不留意会顺带把应用一起删除（见下图）。。\n我刚养了两天的龙虾(OpenClaw) 数据，全没了。。。 呜呜呜~~\n而且本来就在调试，相关应用备份我是一点没准备好。这数据重新弄可老麻烦了（里面还有它刚给我调试好的 Agent web 页面）。\n冷静下来，思索的第一件事是：我有没有快照？能恢复数据不？\n因为我是在虚拟机上安装的1panel和OpenClaw：\n系统备份是4天前的——还原了也用不了 存储上整个iSCSI的块存储（Lun）倒是每天有一个快照——全部还原意味着 8-9台虚拟机的数据也一并还原了。。。 这可咋办？\n最后，在服务不停机的情况下，使用这个快照将应用（OpenClaw）数据恢复到了前一天的状态。\n先介绍下我的测试环境：\n存储端：NAS (Btrfs 文件系统，开启了快照功能) 计算端：VMware vSphere (ESXi 宿主机) 协议：iSCSI 二、 灾难发生的第 1小时：召回“丢失的世界” 不要直接在原始快照上操作，最稳妥的方法是克隆一个临时的 LUN。\n快照定位：在 NAS 管理后台（如 SAN 管理器）找到最近一个完好的快照。\n克隆 LUN：基于快照克隆出一份数据卷（例如命名为 LUN-Recover-Temp）。\n暴露目标：新增一个 iSCSI 目标（给个名字：recovery），并与刚才的克隆 LUN -1关联上。\n为了方便后续挂载，新建iSCSI Target 中的“多重联机”可以开启。\n注意：并确保LUN 策略中放行了 ESXi 宿主机的 IQN。 三、 灾难发生的第 2小时：安全访问克隆数据 这是最惊险的一步，选错选项可能导致克隆卷的数据结构被清空。\n1. 扫描与识别 进入 vCenter -\u0026gt; 主机配置 -\u0026gt; 存储适配器，选择你的 iSCSI 适配器点击 “重新扫描存储”。此时在“设备”列表中应能看到那个克隆出来容量一致的新磁盘。状态是未挂载。\n2. 挂载数据存储（关键！） 点击“新建数据存储”，选择 VMFS 类型，选中那个克隆出来的 LUN。此时向导会弹出 “解析选项”，请屏住呼吸选择：\n❌ 保留现有签名 (Keep Existing Signature) 不选理由：这会保留原始的 UUID。vSphere 会识别出这是一个已有的 VMFS 卷，会覆盖现有挂载。 误选后果：导致其他未受影响的虚拟机数据，一并还原至前一天的状态。——这不是我们希望的。 ✅ 分配新签名 (Assign a new signature)：这通常用于想同时挂载原始和快照卷的情况，会导致虚拟机路径变化，不推荐。 选择理由：当前环境，不希望停止正在运行的虚拟机，只希望把克隆盘中，昨天这台虚拟机中 OpenClaw 的应用数据导出来就行。 ❌ 格式化磁盘 (Format the disk)：点下去你就彻底告别数据了。 成功挂在后，可以看到一个新的“数据存储”，点击存储中“文件”可以看到前一天整个虚拟机的数据。\n四、 虚拟机内部：抢救数据 挂载成功后，浏览数据存储，你会看到“死而复生”的 .vmx 和 .vmdk 文件。到这一步可以说：已经成功一半了。\n注册虚拟机：右键点击恢复出来的 .vmx 文件 -\u0026gt; 注册虚拟机。\n启动：\n启动恢复的虚机：此时可能会提示“I Moved It”或“I Copied It”，选择 “I Copied It” 以新运行一台昨天状态的机器。\n断网/改 IP：进系统后，第一时间修改主机 IP 地址，防止与生产环境冲突。\n因为这里是Linux主机，我就直接用到 nmtui命令快速修改新机器的IP 地址了。\n数据导出与回填：\n因为应用是在 1Panel 下运行的 Docker 服务。\n只需登录进新主机的1Panel管理界面，备份现有应用的应用数据即可。\n随后，下载刚才打包好的备份数据。\n返回登录数据丢失的主机：在 1Panel 中保持 Docker 应用名一致 的情况下，重新导入刚才这个备份数据——\n服务启动：\n随后，服务重新启动。此时除了 服务Tokens 和之前的不一样以外，其他的配置文件，本地记录文件，飞书客户端对接地址。。。都是一样的。 至此，应用就成功恢复了。\n数据还原成功后，克隆虚拟机和克隆卷也就不用了，可以相反的操作回到之前的状态：\n选择“删除虚拟机” \u0026ndash;\u0026gt; “删除数据存储“ \u0026ndash;\u0026gt; 删除iSCSI target \u0026ndash;\u0026gt; 删除 克隆Lun 即可。\n五、 最后的复盘 快照就是生命线：如果你还没在 NAS 上开启定期快照，现在就去点开它。哪怕每天一个快照也行。 1Panel 的优越性：在这种场景下，Docker 应用的备份、还原比直接恢复整台虚机要快得多，也更灵活。 冷静是第一生产力：NAS 和 VMware 都是多重保护的，只要不误点“格式化”或“保留原有标签”，数据总能找回来。 ","date":"2026-03-16T01:06:45+08:00","image":"https://r2.blog.nxlan.cn/PicGorecovery_vm_title.png","permalink":"https://blog.cba.nxlan.cn/p/restore_vm/","title":"生产环境“删库”救命指南：NAS + vSphere 下的虚拟机数据恢复实操记录"},{"content":"最终目标 领取 Google 为开发者赠送的每个月10美元的赠金，并关联到Google Cloud的结算帐号下 （本文内容涵盖） 使用有赠送金的帐号，开启Google AI Studio 的付费API功能，解除API调用Gemini模型时的限制 （本文内容涵盖） 使用有赠送金的帐号，开通Google Cloud 免费额度服务器、安装 1Panel 图形化管理面板 （本文内容涵盖） 使用1Panel 申请https 免费证书，搭建MCP server 供Agent使用 （后续更新，请期待） 效果图 成功领取赠送金\n成功启用付费帐号帐号的API Key\n在Google Cloud上运行MCP server\n准备工作 需要帐号开通google pro 后领取\n有google帐号，且帐号所属区域在美区\n梯子\n国内带有Visa 或 MasterCard 标志的银行卡 （虚拟卡会因为风控 用不了）\n美国免税州地址生成器\n网站: https://www.meiguodizhi.com/usa-address/oregon\n主线流程 登录自己的Google 帐号，确认在美区 登录Google Cloud 激活结算帐号 在结算帐号下领取每月10美元的赠送金 在结算帐号下新建项目并启用 Gemini API 开通 AI Studio 的付费API 功能 开通免费额度服务器 登录进虚拟机，完成1Panel的安装和web初始化 过程\u0026amp;步骤 下面是操作步骤。\n因为涉及google 旗下多个页面的网站，我会复制相关URL 链接在说明文字下方。所以，最好在电脑上查看和操作。\n0. 开启美区的梯子 1. 登录自己的Google 帐号，并确认帐号也在美区 美区可以完整地使用gemini 服务并领取其他优惠福利（如：Google One 学生福利），所以一般建议使用美区帐号操作。\n查看当前Google账号所属地区—— https://policies.google.com/terms?hl=en\n如果不在美区，使用以下链接修改地区—— https://policies.google.com/country-association-form\n美区账户的样子——\n2. 登录Google Cloud 先加入Google 开发者计划，查看是否有赠送的优惠可以领取。\n查看当前帐号是否有优惠——\nhttps://developers.google.com/program/my-benefits?hl=zh-cn\n如果像上图有“每月可获得$10 ”赠金的字样，表示可以领取。\n“无结算帐号”表示当前用户在Google Cloud 中还没有开通有效的结算帐号。\n所以，接下来需要登录 Google Cloud 服务，并开通结算帐号。页面地址如下——\nGoogle Cloud 结算帐号管理地址——\nhttps://console.cloud.google.com/billing\n像上图这样，就是没有有效的结算账户。点击“创建账户”创建 ——\n使用前面提到的 “美国地址生成器” 和 自己的银行卡 信息，完成结算账户创建。\n网站: https://www.meiguodizhi.com/usa-address/oregon\n完成创建后，就进入到“结算账户”的页面中，进行下一步操作。\n3. 激活结算帐号 激活过程中，根据帐号新旧可能不同：\n如果是稳定使用多年的Google 帐号，直接就是激活状态的 如果是Google 帐号刚注册不久或银行卡风险被评估为高，会触发先扣费 10-30 美元验证账户的情况。 ​ 图中就提示需要支付10美元，才能启用该账户 。如果不支付，当前结算账户就是未激活的状态。\n​ 所以，为了后续顺畅使用，会支付这笔验证金。支付后，账户转为“付费账户”的可用状态。\n4. 在结算帐号下领取每月10美元的赠送金 回到之前打开的开发者计划优惠页面：\nhttps://developers.google.com/program/my-benefits?hl=zh-cn\n此时，就可以与上一步骤中创建好结算帐号（“我的结算帐号”）完成关联。\n点击“管理赠金”，就会看到赠送的 10 美元已经到帐了 ，并且关联到帐号“我的结算帐号”。\n5. 在结算帐号下新建项目并启用 Gemini API 在Google Cloud下的“API 和服务”中， 选择或创建项目——\n网址：https://console.cloud.google.com/projectselector2/apis/dashboard\n随后在 搜索中找到 Gemini API——\n启用它——\n6. 开通 AI Studio 的付费API 功能 登录AI studio ——\n网址： https://aistudio.google.com/prompts/new_chat\n点击 “NO API Key”，进入关联付费API 账户的页面。\n按照以下步骤，逐步关联付费项目，并创建Gemini API key——\n最后，确认。注意：不要勾选“付费 API Key”。\n点击“Get API key”可以看到刚才创建的API Key——\n该API key 不仅可以在 AI studio 中使用，还可以用在第三方应用（例如：下图的Chatbox）——\n7. 开通免费额度服务器 首先需要了解，哪些类型的资源是免费额度的。\n这里截取官方说明：\n免费额度说明：\nhttps://docs.cloud.google.com/free/docs/free-cloud-features?hl=zh-cn#free-tier-usage-limits\n所以，一会我们开通Google Cloud 上虚拟机的时候，需要选择 us-west1 的机器（俄勒冈），使用30GB的标准永久磁盘，实例类型需要是e2-micro （2core 1G），并尽量避免过多的到中国数据出站。\n具体操作步骤：\n7.1 创建虚拟机实例 7.2 选择机器配置 [机器配置]：\n机器区域，选免费额度的us-west1(俄勒冈)\n机器类型选免费额度的e2-micro\n[系统和存储]：\n系统选Ubuntu。注意：磁盘要改成免费额度的“标准永久性磁盘”，大小控制在30GB以内。\n[数据保护]：\n数据保护，选择无备份无复制：\n一来是因为，快照和跨区域备份都会产生费用；二来是因为，我们的配置和数据可以在1Panle 中通过图形化导出、备份。——即应用层级的备份，所以不使用这里系统级别的备份也是可行的。\n[网络]：\n网络这里需要设置两个地方——放行http和https的流量。\nIP栈选择 IPv4，IP 类型要改为临时，服务层级同样是 us-west1。\n[其他设置项]：\n其他设置项目（可观测性、安全、高级）中保持默认——非必要不开启。\n最后，点击“创建”。\n7.3 查看机器信息 机器创建成功后，可以看到机器名和外部IP。还有ssh 登录连接。\n其中，“外部IP” 是一会我们从浏览器中访问的IP地址以及 后续域名映射时用到的外部IP。\n“查看结算报告”可以统计最近一个月机器的使用情况，理论上应该像我这样费用为0——\nSSH这里就是一会通过网页登录到虚拟机上操作的链接。后续会详细说明。\n7.4 登录虚拟机 虚拟机创建好了，后续就是远程登录、操作这台虚拟机了。\n有两个方法登录到这台虚拟机——\n长期办法： 将自己本地的ssh 会话密钥导入到虚拟机用户.ssh 文件夹内。（下一篇文章会用到）\n临时办法： 通过浏览器打开ssh 会话 或 通过google shell 跳转登录到这台机器上。 （本文使用）\n我使用中，点击“浏览器窗口中打开”ssh 会话，一直无响应。\n所以，使用了另一种方法 ：通过google cloud shell 登录机器。\n点击上面的“查看gcloud命令”，可以得到一段命令。\n复制这段命令后，点击“在 Cloud Shell 中运行”，就会进入google cloud shell 界面。\n8. 登录进虚拟机，完成1Panel的安装和web初始化 在cloud shell 中粘贴刚才的命令，就可以成功登陆进虚拟机 。\n登录到虚拟机后，使用下面的安装脚本，进行1Panle 工具的安装和启动。\n在线安装 - 1Panel 文档\n1 bash -c \u0026#34;$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)\u0026#34; 安装成功后，出现外部地址和 用户、密码 等信息——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [1Panel 2026-02-24 13:19:15 install Log]: =================感谢您的耐心等待，安装已完成================== [1Panel 2026-02-24 13:19:15 install Log]: [1Panel 2026-02-24 13:19:15 install Log]: 请使用您的浏览器访问面板: [1Panel 2026-02-24 13:19:15 install Log]: 外部地址: http://1.162.151.19:11634/c342b65410 [1Panel 2026-02-24 13:19:15 install Log]: 内部地址: http://192.168.202.57:11634/c342b65410 [1Panel 2026-02-24 13:19:15 install Log]: 面板用户: 7b95048278 [1Panel 2026-02-24 13:19:15 install Log]: 面板密码: 1a3a2e26ce [1Panel 2026-02-24 13:19:15 install Log]: [1Panel 2026-02-24 13:19:15 install Log]: 官方网站: https://1panel.cn [1Panel 2026-02-24 13:19:15 install Log]: 项目文档: https://1panel.cn/docs [1Panel 2026-02-24 13:19:15 install Log]: 代码仓库: https://github.com/1Panel-dev/1Panel [1Panel 2026-02-24 13:19:15 install Log]: 前往 1Panel 官方论坛获取帮助: https://bbs.fit2cloud.com/c/1p/7 [1Panel 2026-02-24 13:19:15 install Log]: [1Panel 2026-02-24 13:19:15 install Log]: 如果您使用的是云服务器，请在安全组中打开端口 11634 [1Panel 2026-02-24 13:19:15 install Log]: [1Panel 2026-02-24 13:19:15 install Log]: 为了您的服务器安全，离开此屏幕后您将无法再次看到您的密码，请记住您的密码。 [1Panel 2026-02-24 13:19:15 install Log]: [1Panel 2026-02-24 13:19:15 install Log]: ================================================================ 建议：复制一份。稍候登录时 会使用。\n这里还有个提醒：“如果您使用的是云服务器，请在安全组中打开端口 11634 ”。\n所以，登录1panel 服务前还需要 在Google Cloud 中**追加一条策略 **: 允许公网访问虚拟机外部IP 的11634 tcp服务——\n防火墙管理页面——\nhttps://console.cloud.google.com/net-security/firewall-manager/\n“来源地址范围”这里，如果你的网络环境有固定IP就填对应的固定IP，如果没有固定IP（例如家庭网络环境）就填写 \u0026ldquo;0.0.0.0/0\u0026rdquo;——\n点击“创建”完成这条防火墙策略的追加。\n同时，在“防火墙政策”中可以看到新增了一条 入站方向，目标协议为 tcp:11634 的放行策略。\n最后，使用1Panel 安装时的外部地址（例如： http://1.162.151.19:11634/c342b65410 ）登录到1Panle 的web 管理页面——\n小结 至此，就完成了本文开头提到的任务目标：\n领取 Google 为开发者赠送的每个月10美元的赠金，并关联到Google Cloud的结算帐号下 （本文内容涵盖） 使用有赠送金的帐号，开启Google AI Studio 的付费API功能，解除API调用Gemini模型时的限制 （本文内容涵盖） 使用有赠送金的帐号，开通Google Cloud 免费额度服务器、安装 1Panel 图形化管理面板 （本文内容涵盖） 使用1Panel 申请https 免费证书，搭建MCP server 供Agent使用 （后续更新，请期待） Q\u0026amp;A （个别支线流程） Q：如果发现：帐号已经切换到美区了，梯子也在美国，但还是无法访问 Gemini 应用 (https://gemini.google.com/app)。\nA：可能是你的帐号之前有其他区的付款帐号存在，需要在以下支付账户管理页面中先删除非美国区的支付账户。\nhttps://payments.google.com/gp/w/home/settings\nQ：如果在创建“支付帐号”时使用了虚拟卡，在绑卡阶段会报错：\nA：测试下来，是可以使用国内的Visa 或 MasterCard 信用卡绑定的。比起虚拟卡，直接用国内的双币卡还简单些。\nQ：开通付费结算帐号后，最好设置下费用提醒。以免发生超额使用，也不知情的情况。\n​\tA：具体操作路径在 “结算” \u0026ndash;\u0026gt; “ 预算和提醒” \u0026ndash;\u0026gt; \u0026ldquo;创建预算\u0026quot;下：\n​\t设置每月预算金额就靠近 赠送额度上限，例如：9.5美金：\n​\t系统会自动根据设置金额，计算提醒阈值。例如：实际支出达到 50% 90% 和 100%时都会收到通知提醒。\nQ：开通了开发者帐号后，为什么只有第一个月有$10 赠送金，不是说可以连续领取12个月么？\nA：出现这种情况是因为：\n开通开发者帐号时，使用的google pro的身份不是来自自己帐号，而是从家庭帐号分享出来的。\n此时，家庭组成员的开发者帐号只能领取一个月的赠送金额。\n","date":"2026-02-25T12:24:03+08:00","image":"https://r2.blog.nxlan.cn/PicGoTitle_image.png","permalink":"https://blog.cba.nxlan.cn/p/setup_gemini_api/","title":"领取Google 开发者赠金，并开通Gemini API和免费云主机"},{"content":"本期主角 [ Chrome MCP Server ]\n让AI接管你的浏览器，将您的浏览器转变为强大的 AI 控制自动化工具。 话不多说，直接开干！\n准备工作 Windows 下需要提前准备以下内容：\n名称 描述 链接 nodejs 和 npm nodejs 和 包管理工具 https://nodejs.org/dist/v24.12.0/node-v24.12.0-x64.msi chrome 浏览器 google chrome 浏览器 Google Chrome – download python python 程序和运行时 貌似 在安装nodejs 的相关工具时，会自动更新 python 环境，等待安装完成就好 注意：\n安装 nodejs 时，要勾选自动安装必要环境组件的选项。否则安装完nodejs 和 npm 后还是会缺少 vs tools 等工具。\n勾选后，会自动进行下载安装相关工具，例如：python、 Microsoft Visual C++、chocolatey 等。\n准备工作完成：\n项目代码：\nChrome MCP Server\nchrome-mcp-server-1.0.0.zip\n执行安装步骤： 打开chrome，语言设置为【中文】 在chrome 扩展中心，开启开发者模式 选择“加载未打包的扩展程序”，安装上面下载好的 chrome-mcp-server 插件包（需先解压） 安装 依赖包\n1 npm install -g mcp-chrome-bridge 启动 chrome-mcp-server。\n启动成功的话，会提示“服务运行中”\n复制 上述MCP server 配置文件\n1 2 3 4 5 6 7 8 { \u0026#34;mcpServers\u0026#34;: { \u0026#34;streamable-mcp-server\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;streamable-http\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;http://127.0.0.1:12306/mcp\u0026#34; } } } 在chatbox 中使用Browser use 工具 当然也可以在，支持MCP client 的Trace Cursor等工具上使用。\n在chatbox 的mcp 设置中，配置工具：\n在chatbox 中，新建对话。关联刚新建的工具： streamable-mcp-server ，模型使用deepseek-chat。\n通过文本方式，告诉需要操作的任务——\n然后将看到 LLM 调用mcp 工具，完成chrome 操作 反思 执行任务的每一步：\n","date":"2026-02-11T12:04:13+08:00","image":"https://r2.blog.nxlan.cn/PicGoimage-20260107101810905.png","permalink":"https://blog.cba.nxlan.cn/p/local_browser_use/","title":"Browser use 本地使用案例"},{"content":"前情提要： 最近，把TrueNAS 中的app traefik 升级（2.11 \u0026ndash;\u0026gt; 3.5.4 ）。\n结果发现，使用traefik反代的https流量，证书都变为traefik 的自签名证书了 ，修改app 中的证书关联项也不成功。\n而升级前，app 中都是可以关联我通过ACME 自动更新的签名证书 。\n注意：\n我的TrueNAS 版本是： TrueNAS-SCALE-23.10.2\n该版本还在使用K3S 管理app服务。\n于是又有了一篇，折腾后的文章。\n使用K3S secret的办法手动指定证书 优点 符合 Kubernetes 最佳实践 证书存储在 Kubernetes 内部，更安全 更新证书只需更新 Secret，无需重启 Traefik 应用配置 可以在多个 IngressRoute 中重复使用 缺点 需要手动创建和管理 Secret 执行以下步骤创建traefik 的secret TrueNAS 系统的证书文件都在 /etc/certificates/ 文件夹下。\n这里主要用到的是 ACME域名证书。\n1. 创建 Secret 1 2 3 4 k3s kubectl create secret tls traefik-default-cert \\ --namespace=ix-traefik \\ --cert=/etc/certificates/ACME_Certificate.crt \\ --key=/etc/certificates/ACME_Certificate.key 2. 验证 Secret 1 2 k3s kubectl get secret traefik-default-cert -n ix-traefik k3s kubectl describe secret traefik-default-cert -n ix-traefik 日志：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026gt; k3s kubectl get secret traefik-default-cert -n ix-traefik -o yaml apiVersion: v1 data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2RENDQTBLZ0F3SUJBZ0lTQm9lTXdKYUxkZ2V2Mk1DZXBiSTZ0WEt0T......VJUSUZJQ0FURS0tLS0t tls.key: LS0tLS1CR......S0VZLS0tLS0K kind: Secret metadata: creationTimestamp: \u0026#34;2026-01-19T10:22:48Z\u0026#34; name: traefik-default-cert namespace: ix-traefik resourceVersion: \u0026#34;114365208\u0026#34; uid: d54fd403-330a-4e43-8104-1188ced48c83 type: kubernetes.io/tls 测试是否生效 1. 创建一个测试 IngressRoute 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat \u0026gt; test-tls.yaml \u0026lt;\u0026lt; EOF apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: test-cert namespace: ix-traefik spec: entryPoints: - websecure routes: - match: Host(\u0026#39;test.home.nxlan.cn\u0026#39;) kind: Rule services: - name: whoami port: 80 tls: secretName: traefik-default-cert EOF 2. 应用上述测试配置 1 k3s kubectl apply -f test-tls.yaml 3. 使用curl 测试证书生效情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 root@truenas[/etc/certificates]# curl -v https://test.home.nxlan.cn --insecure * Trying 192.168.202.6:443... * Connected to test.home.nxlan.cn (192.168.202.6) port 443 (#0) * ALPN: offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 * ALPN: server accepted h2 * Server certificate: * subject: CN=home.nxlan.cn * start date: Dec 22 01:53:44 2025 GMT * expire date: Mar 22 01:53:43 2026 GMT * issuer: C=US; O=Let\u0026#39;s Encrypt; CN=E7 * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * using HTTP/2 * h2h3 [:method: GET] * h2h3 [:path: /] * h2h3 [:scheme: https] * h2h3 [:authority: test.home.nxlan.cn] * h2h3 [user-agent: curl/7.88.1] * h2h3 [accept: */*] * Using Stream ID: 1 (easy handle 0x56179e69fc90) \u0026gt; GET / HTTP/2 \u0026gt; Host: test.home.nxlan.cn \u0026gt; user-agent: curl/7.88.1 \u0026gt; accept: */* \u0026gt; * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): \u0026lt; HTTP/2 404 \u0026lt; content-type: text/plain; charset=utf-8 \u0026lt; x-content-type-options: nosniff \u0026lt; content-length: 19 \u0026lt; date: Mon, 19 Jan 2026 12:08:54 GMT \u0026lt; 404 page not found * Connection #0 to host test.home.nxlan.cn left intact 其中证书部分时间可以看出，ACME 证书已经生效了——\nServer certificate:\nsubject: CN=home.nxlan.cn start date: Dec 22 01:53:44 2025 GMT expire date: Mar 22 01:53:43 2026 GMT issuer: C=US; O=Let\u0026rsquo;s Encrypt; CN=E7 在app 中关联secret 证书 1. 点击TrueNAS 中的 “应用” 2. 编辑已关联traefik 的app，找到“TLS-Settings” 部分，关联刚设置好的secret 应用生效。\n3. 访问app web 页面验证生效 浏览器不再提示“证书错误”，至此所有操作完成。\n追加补充： 上面追加的配置文件，不会随着证书更新而自动更新。\n所以，需要定期刷新 这个自定义证书配置。刷新的方法 也很简单——使用计划任务定期执行shell 脚本。\n1. shell脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 cat /usr/local/bin/update-cert.sh =================================== #!/bin/bash # 脚本路径和命名空间 NAMESPACE=\u0026#34;ix-traefik\u0026#34; SECRET_NAME=\u0026#34;traefik-default-cert\u0026#34; CERT_PATH=\u0026#34;/etc/certificates/ACME_Certificate.crt\u0026#34; KEY_PATH=\u0026#34;/etc/certificates/ACME_Certificate.key\u0026#34; # 删除旧的 secret k3s kubectl delete secret ${SECRET_NAME} --namespace=${NAMESPACE} # 用新文件创建 secret k3s kubectl create secret tls ${SECRET_NAME} \\ --namespace=${NAMESPACE} \\ --cert=${CERT_PATH} \\ --key=${KEY_PATH} echo \u0026#34;Secret ${SECRET_NAME} in namespace ${NAMESPACE} has been updated.\u0026#34; 2. 计划任务 \u0026ldquo;系统\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;高级\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;编辑定时任务\u0026rdquo; 中，追加上述脚本。\n我这里设置为 每月2号，更新证书配置“traefik-default-cert”。\n","date":"2026-01-28T12:34:17+08:00","image":"https://r2.blog.nxlan.cn/PicGoK3S_updatecert.png","permalink":"https://blog.cba.nxlan.cn/p/k3s_app_setclustercert/","title":"K3S中使用 traefik 给App关联ACME证书 "},{"content":"前情提要： 之前在vCenter 中使用 iSCSI 提供统一存储服务都是好好的。 后来 想着在本地windows PC 上也使用iSCSI，方便存储一些 不常用的文件。 结果，因为本地有两个PC 同时访问一个 iSCSI 盘的情况（想着游戏盘 就共享了）。但是，两台机器同时有操作 就会发现有文件损坏or 丢失的情况。 修复 iSCSI 盘中 “无法枚举容器中的对象，访问被拒绝”的问题 现象：\nPC-B 复制已经下载好的 ollama 模型到共享盘 \u0026ldquo;N:\\Ollama_models\u0026quot;后，PC-A 读取这个文件夹时报错：\n“windows 文件夹权限提示：“无法枚举容器中的对象，访问被拒绝”\n具体修复动作如下：\n1. 先修复文件夹所有者 1 takeown /f \u0026#34;N:\\Ollama_models\u0026#34; /r /d y 执行记录：\n1 2 3 成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-05a61d37b08453e59290add468e3bb2f688e23a01e967fecb0e2fa41218cea76\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-06507c7b42688469c4e7298b0a1e16deff06caf291cf0a5b278c308249c3e439\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-0a3d61b01340ca9dceb4a661e21b3dfe1418ed5206d91291ab933a3762f8bb89\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-3a18673ff291a1d8de94d490877127899356d33a18028d5f3945bf245c11b02c\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-3fcd3febec8b3fd64435204db75bf0dd73b91e8d0661e0331acfe7e7c3120b85\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-59bb50d8116b6a1f9bfbb940d6bb946a05554e591e30c8c2429ed6c854867ecb\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-6e4c38e1172f42fdbff13edf9a7a017679fb82b0fde415a3e8b3c31c6ed4a4e4\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-772f510b95588aeb9fbd2298b2b647bceba48aceb05e3a26ff14812eb1f6dc14\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-8893e08fa9f91f7dc39e24d27bdfaece4e9c86bb3269293ff8cea6cba98c872d\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-8972a96b8ff1957ca24ff839aeb54411e6849de68609857a3fa17a4e78114247\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-9202febed9e2dadac14bca089be90864571336fa9f4375b690a26ed548957fde\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-a3a0e9449cb691a12f4de1d03725fd41326614fdeaf5d80b28c51187da0bed0e\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-a3de86cd1c132c822487ededd47a324c50491393e6565cd14bafa40d0b8e686f\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-a406579cd136771c705c521db86ca7d60a6f3de7c9b5460e6193a2df27861bde\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-ae370d884f108d16e7cc8fd5259ebc5773a0afa6e078b11f4ed7e39a27e0dfc4\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-ae40a217c1c4002e9358f0f6597a349acaace0cfb95dc53db7ce646d57a56271\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-c5ad996bda6eed4df6e3b605a9869647624851ac248209d22fd5e2c0cc1121d3\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-c8efaf6dac5aab4dc1030895032f0f028d7835348bdb21d4aebb89cda5788fe5\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-cff3f395ef3756ab63e58b0ad1b32bb6f802905cae1472e6a12034e4246fbbdb\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-e6a7edc1a4d7d9b2de136a221a57336b76316cfe53a252aeba814496c5ae439d\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-ed8474dc73db8ca0d85c1958c91c3a444e13a469c2efb10cd777ca9baeaddcb7\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\blobs\\sha256-f64cd5418e4b038ef90cf5fab6eb7ce6ae8f18909416822751d3b9fca827c2ab\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所 有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-coder\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-r1\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-coder\\6.7b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr\\3b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr\\latest\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-r1\\8b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3\\8b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding\\0.6b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。成功: 此文件(或文件夹): \u0026#34;N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding\\8b\u0026#34; 现在由用户 \u0026#34;DESKTOP-GVTOPMJ\\gg\u0026#34; 所有。 C:\\WINDOWS\\system32\u0026gt; 2. 再修复文件权限 1 icacls \u0026#34;N:\\Ollama_models\u0026#34; /reset /t /c /l 执行记录：\n1 C:\\WINDOWS\\system32\u0026gt;已处理的文件: N:\\Ollama_models已处理的文件: N:\\Ollama_models\\blobs已处理的文件: N:\\Ollama_models\\manifests已处理的文件: N:\\Ollama_models\\blobs\\sha256-05a61d37b08453e59290add468e3bb2f688e23a01e967fecb0e2fa41218cea76已处理的文件: N:\\Ollama_models\\blobs\\sha256-06507c7b42688469c4e7298b0a1e16deff06caf291cf0a5b278c308249c3e439已处理的文件: N:\\Ollama_models\\blobs\\sha256-0a3d61b01340ca9dceb4a661e21b3dfe1418ed5206d91291ab933a3762f8bb89已处理的文件: N:\\Ollama_models\\blobs\\sha256-3a18673ff291a1d8de94d490877127899356d33a18028d5f3945bf245c11b02c已处理的文件: N:\\Ollama_models\\blobs\\sha256-3fcd3febec8b3fd64435204db75bf0dd73b91e8d0661e0331acfe7e7c3120b85已处理的文件: N:\\Ollama_models\\blobs\\sha256-59bb50d8116b6a1f9bfbb940d6bb946a05554e591e30c8c2429ed6c854867ecb已处理的文件: N:\\Ollama_models\\blobs\\sha256-6e4c38e1172f42fdbff13edf9a7a017679fb82b0fde415a3e8b3c31c6ed4a4e4已处理的文件: N:\\Ollama_models\\blobs\\sha256-772f510b95588aeb9fbd2298b2b647bceba48aceb05e3a26ff14812eb1f6dc14已处理的文件: N:\\Ollama_models\\blobs\\sha256-8893e08fa9f91f7dc39e24d27bdfaece4e9c86bb3269293ff8cea6cba98c872d已处理的文件: N:\\Ollama_models\\blobs\\sha256-8972a96b8ff1957ca24ff839aeb54411e6849de68609857a3fa17a4e78114247已处理的文件: N:\\Ollama_models\\blobs\\sha256-9202febed9e2dadac14bca089be90864571336fa9f4375b690a26ed548957fde已处理的文件: N:\\Ollama_models\\blobs\\sha256-a3a0e9449cb691a12f4de1d03725fd41326614fdeaf5d80b28c51187da0bed0e已处理的文件: N:\\Ollama_models\\blobs\\sha256-a3de86cd1c132c822487ededd47a324c50491393e6565cd14bafa40d0b8e686f已处理的文件: N:\\Ollama_models\\blobs\\sha256-a406579cd136771c705c521db86ca7d60a6f3de7c9b5460e6193a2df27861bde已处理的文件: N:\\Ollama_models\\blobs\\sha256-ae370d884f108d16e7cc8fd5259ebc5773a0afa6e078b11f4ed7e39a27e0dfc4已处理的文件: N:\\Ollama_models\\blobs\\sha256-ae40a217c1c4002e9358f0f6597a349acaace0cfb95dc53db7ce646d57a56271已处理的文件: N:\\Ollama_models\\blobs\\sha256-c5ad996bda6eed4df6e3b605a9869647624851ac248209d22fd5e2c0cc1121d3已处理的文件: N:\\Ollama_models\\blobs\\sha256-c8efaf6dac5aab4dc1030895032f0f028d7835348bdb21d4aebb89cda5788fe5已处理的文件: N:\\Ollama_models\\blobs\\sha256-cff3f395ef3756ab63e58b0ad1b32bb6f802905cae1472e6a12034e4246fbbdb已处理的文件: N:\\Ollama_models\\blobs\\sha256-d18a5cc71b84bc4af394a31116bd3932b42241de70c77d2b76d69a314ec8aa12已处理的文件: N:\\Ollama_models\\blobs\\sha256-e6a7edc1a4d7d9b2de136a221a57336b76316cfe53a252aeba814496c5ae439d已处理的文件: N:\\Ollama_models\\blobs\\sha256-ed8474dc73db8ca0d85c1958c91c3a444e13a469c2efb10cd777ca9baeaddcb7已处理的文件: N:\\Ollama_models\\blobs\\sha256-f64cd5418e4b038ef90cf5fab6eb7ce6ae8f18909416822751d3b9fca827c2ab已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-coder已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-r1已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-coder\\6.7b已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr\\3b已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-ocr\\latest已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\deepseek-r1\\8b已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3\\8b已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding\\0.6b已处理的文件: N:\\Ollama_models\\manifests\\registry.ollama.ai\\library\\qwen3-embedding\\8b已成功处理 40 个文件; 处理 0 个文件时失败 3. 执行完上述修复，再次操作验证 PC-A 上再次读取执行 N:\\Ollama_models 就正常了。\n回到标题，为什么这个问题根因不在iSCSI上？ 因为NTFS 不是一个集群文件系统。 它被设计为一次只能由一台计算机（一个操作系统实例）独占访问和管理。当第二台计算机试图挂载同一个NTFS卷时，它无法感知第一台计算机的操作，反之亦然。\n两台PC 同时操作一个NTFS 盘会发生什么？\n数据损坏（最可能且最严重）：\n写入覆盖：PC-A 和 PC-B 可能同时修改同一个文件的同一部分，或一个在写入而另一个在读取。最终写入磁盘的数据将是混乱的，文件内容被破坏。\n元数据损坏：NTFS 的元数据（如主文件表 MFT、位图、日志）是文件系统的“目录和账本”。两台机器同时更新这些关键数据结构（例如，创建/删除文件、扩展文件大小），会立即导致文件系统逻辑混乱，可能使整个卷无法识别。\n缓存一致性问题：\n每台PC都会在内存中缓存文件和元数据以提高性能。一台PC对文件的修改会停留在自己的缓存中，不会立即通知另一台PC。另一台PC读取到的是磁盘上的旧数据或自己缓存中的旧数据，导致数据视图不一致。\n系统不稳定和蓝屏：\n当Windows检测到文件系统出现严重逻辑错误或无法理解的元数据状态时（例如，它认为应该空闲的块却被另一台机器分配了），可能会抛出文件系统错误，导致应用程序崩溃，甚至引发系统蓝屏死机。\n“脏卷”和强制检查：\n当一台PC正常卸载卷（或关机）时，它会执行清理操作，将缓存写入磁盘并标记卷为“干净”状态。\n如果另一台PC还在使用，当它最终卸载时，或系统意外崩溃，卷会被标记为“脏”。下次任何一台PC尝试挂载时，Windows 的 chkdsk 会强制运行以尝试修复，但修复过程很可能导致数据丢失或损坏，因为它无法理解由并发访问造成的复杂损坏。\n也就是说，chkdsk 修复数据也不是万能的，首先它的有正确的元数据信息 。\n如果，这里的元数据损坏，chkdsk 修复可能只是让它 看起来可以读取、写入了。\n至于，文件数据是否和之前的一致，它也是不确定的。\n例如，修复后的照片成了这样——\n而同样使用 iSCSI 协议的VMFS 文件系统就不会发生这样的问题 首先，VMFS 文件系统在设计目标中就为了多主机并发访问使用——\nVMFS 如何安全地实现多主机并发访问？\n元数据操作串行化：\n当一台ESXi主机需要创建或删除虚拟机文件时，它会向存储设备发起一个 SCSI预留（一种锁），短暂地独占访问权以更新VMFS的元数据。操作完成后立即释放。其他主机会等待。\n这保证了文件系统结构（“账本”）永远不会被同时篡改。\n数据操作允许并行：\n对于虚拟机虚拟磁盘文件（VMDK）内部的数据读写，多台主机可以同时进行。因为每台主机操作的是文件内不同的偏移量块，VMFS不需要为此上全局锁。\n这实现了高性能的并发虚拟机运行。\n心跳和脑裂保护：\nVMFS在存储上有一个小的区域用于存储“心跳”。主机定期在此写入信息，表明自己“存活”。\n如果一台主机故障，其他主机能通过心跳超时检测到，并可以安全地接管其资源，避免出现“脑裂”（两台主机都以为自己是唯一操作者）\n如果对比 NTFS 和 VMFS 文件系统——\n特性 NTFS VMFS 设计目标 单台物理机或虚拟机的本地文件系统 专为多台ESXi主机并发访问共享存储而设计的集群文件系统 并发访问 不支持。没有内置的分布式锁机制。 核心功能。内置了分布式锁管理器和原子操作。 元数据保护 假设自己独占磁盘，直接修改。 使用磁盘心跳区、SCSI预留、原子测试与设置等机制，在修改关键元数据（如文件目录、空间位图）前，会先“上锁”，确保同一时刻只有一台主机能进行修改。 缓存一致性 每台机器的缓存独立，无同步。 设计时考虑了缓存失效机制。虽然数据缓存主要在主机内存，但元数据变更会通过存储网络通知其他主机使其缓存失效或更新。 适用场景 Windows/Linux本地磁盘，或一对一的iSCSI连接。 多对一的共享存储场景，典型如vSphere集群、多主机同时运行其上的虚拟机。 所以说，共享块存储本身没有问题（iSCSI/FC），问题在于其之上运行的文件系统是否支持多节点并发访问。\n这里的案例就是，在感觉vmware 可行，那windows 也可行认知下的错误做法。\n错误做法：共享块存储 + 非集群文件系统 = 数据灾难。 正确的解决方案 如果你需要让多台终端共享访问同一个存储池，必须使用正确的技术栈：\nWindows Server Failover Cluster (WSFC) + CSV：\n这是微软官方的解决方案。多台服务器组成故障转移集群，使用集群共享卷。\n关键点：在任何时刻，CSV 卷的NTFS元数据操作仍由一台“协调节点”控制，其他节点通过它进行协调，从而保证一致性。数据IO可以直接访问。这实现了真正的多节点并发读写，但需要Windows Server集群环境。\n使用真正的集群文件系统：\n例如用于Linux的 OCFS2、GFS2，或跨平台的 VMFS（VMware专用）。 注意：Windows原生没有类似广泛使用的通用集群文件系统。 windows下，考虑改为文件级共享：\n如果你想在两台windows PC之间共享文件，绝对不应该使用iSCSI。\n正确的做法是：\n将iSCSI存储挂载给其中一台PC（例如PC-A）。 在PC-A上将此NTFS卷设置为共享文件夹（SMB/CIFS协议）。 PC-B通过网络共享（如 \\\\PC-A\\ShareName）来访问文件。 这样，文件锁、缓存和一致性都由PC-A上的Windows文件服务器服务来统一管理，完全安全。 ","date":"2026-01-28T11:02:35+08:00","image":"https://r2.blog.nxlan.cn/PicGoiscsi_img.png","permalink":"https://blog.cba.nxlan.cn/p/iscsi_share/","title":"iSCSI 文件共享冲突——这可能不是iSCSI 的锅。。。"},{"content":"LangChain 演示项目 最近整理了一个基于 LangChain 的智能代理（Agent）演示项目，展示了如何使用 LangChain 构建功能丰富的 AI Agent系统。\n参考: 为什么复杂AI项目要用LangChain？\n项目概述 本项目包含多个示例，逐步展示了 LangChain 的核心功能，从基础的代理构建到高级的记忆管理、中间件集成等。项目使用 DeepSeek 和 Ollama 作为 LLM 提供商，并集成了多种工具和功能。\n主要特性 多 LLM 支持: 支持 DeepSeek 和 Ollama 两种 LLM 提供商 工具集成: 包含订单查询、退货处理、邮件发送、网络搜索等多种工具 中间件系统: 演示了回调、PII 保护、自定义中间件等功能 记忆管理: 包含短期记忆和基于 PostgreSQL 的长期记忆 结构化输出: 支持自动结构化和工具结构化 人机交互: 支持人类在环（Human-in-the-loop）的工作流 项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 langchain_demo/ ├── llm.py # LLM 配置（DeepSeek 和 Ollama） ├── tools.py # 工具定义（订单、邮件、搜索等） ├── 01_langchain_with_tools.py # 基础代理与工具 ├── 02_langchain_with_Callback.py # 回调中间件 ├── 03_langchain_with_Middleware.py # PII 保护中间件 ├── 04_langchain_with_Summarization.py # 摘要功能 ├── 05_langchain_with_Humaninloop.py # 人机交互 ├── 06_langchain_with_Customize_Middleware.py # 自定义中间件 ├── 07_langchain_with_Autostructur.py # 自动结构化 ├── 08_langchain_with_Toolstructur.py # 工具结构化 ├── 09_langchain_with_Providerstructur.py # 提供商结构化 ├── 10_langchain_with_shortMemory.py # 短期记忆 ├── 11_langchain_with_longMemory.py # 长期记忆（PostgreSQL） ├── 11_docker-compose.yml # PostgreSQL Docker 配置 ├── 11_init-db.sh # 数据库初始化脚本 └── README.md # 本文档 需要具体代码，可以关注公众号后，私信【0121】获取。\n","date":"2026-01-21T12:15:37+08:00","image":"https://r2.blog.nxlan.cn/PicGolangchain_demo_outline.png","permalink":"https://blog.cba.nxlan.cn/p/langchain_demo/","title":"LangChain 演示项目"},{"content":"补充 Tools 细节 通过上篇文章，我们知道：AI 模型在垂直领域和及时更新的知识，依赖于外部工具执行后的结果（比如，明天北京天气，浪浪山中的动画人物形象）。\n并且把执行结果追加到上下文中，大模型才会基于相关内容做进一步的动作 。\n为了方便对比，把上一篇案例中的tools.py 部分代码 单独放一下——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import os from tavily import TavilyClient tavily_client = TavilyClient(api_key=os.getenv(\u0026#34;TAVILY_API_KEY\u0026#34;)) tools = [ { \u0026#34;name\u0026#34;: \u0026#34;tavily_search\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;使用 Tavily 进行网络搜索，获取相关信息\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;query\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;搜索查询字符串\u0026#34;, } }, \u0026#34;required\u0026#34;: [\u0026#34;query\u0026#34;] }, }, ] def tavily_search(query: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; 使用 Tavily 进行网络搜索并返回搜索结果。 参数: query (str): 搜索查询字符串 返回: str: 搜索结果的字符串表示 \u0026#34;\u0026#34;\u0026#34; try: response = tavily_client.search(query, search_depth=\u0026#34;advanced\u0026#34;, max_results=5) results = response.get(\u0026#34;results\u0026#34;, []) if not results: return \u0026#34;未找到相关搜索结果。\u0026#34; # 格式化搜索结果 formatted_results = [] for result in results: title = result.get(\u0026#34;title\u0026#34;, \u0026#34;无标题\u0026#34;) url = result.get(\u0026#34;url\u0026#34;, \u0026#34;\u0026#34;) content = result.get(\u0026#34;content\u0026#34;, \u0026#34;\u0026#34;) formatted_results.append(f\u0026#34;标题: {title}\\n链接: {url}\\n内容: {content}\\n\u0026#34;) return \u0026#34;\\n\u0026#34;.join(formatted_results) except Exception as e: return f\u0026#34;搜索过程中发生错误: {str(e)}\u0026#34; \u0026ldquo;tools\u0026rdquo; 变量是个对象列表，用来填充提示词 中{tools} 部分 为什么需要这部分？\n原因也不复杂：在大模型不支持 function calling之前，只能通过这种提示词注入的方式告诉模型：有什么工具可以调用以及如何调用。\n而当大模型支持 function calling 后，这个步骤没有省略——而是从提示词中迁移到大模型提供的**专门接口“tools”**中。\n长这样——\n结构上看，只是比上文的tools 多了一层封装——{\u0026ldquo;type\u0026rdquo;: \u0026ldquo;function\u0026rdquo;, \u0026ldquo;function\u0026rdquo;: {原工具提示词部分内容} }\n回到 \u0026ldquo;def tavily_search(query: str)\u0026rdquo; 这部分 不难看出：所谓的\u0026quot;工具\u0026quot;本质上也是一个函数。\n那LLM是怎么“调用”tools执行具体任务的呢？\n在大模型不支持 function calling之前：就以LLM 输出的 \u0026ldquo;Action：XXX\u0026rdquo; 内容为条件，做if-else 选择。 ​\t简化后逻辑是这样——\n1 2 3 if ( LLM 输出 \u0026#34;Action: tavily_search\u0026#34; ): tavily_search( query = \u0026#34;Action Input: 工具参数\u0026#34; ) 当大模型支持 function calling后，大模型则直接在\u0026quot;tool_calls\u0026ldquo;中返回它希望调用的具体工具（函数）名和相关参数。长这样—— 工具执行阶段：只需调用\u0026quot;name\u0026quot;指定的函数，按照 \u0026ldquo;arguments\u0026rdquo; 中的参数执行——\n1 2 3 4 5 6 7 for tool_call in response.tool_calls: tool_name = tool_call[\u0026#34;name\u0026#34;] tool_args = tool_call[\u0026#34;args\u0026#34;] get_tool = tools_with_name[tool_name] print(f\u0026#34;调用工具：{tool_name}, {tool_args}\u0026#34;) # 执行工具函数（同步） call_tool_ret = get_tool.invoke(tool_args) 好，以上补充了 tools 在Agent中执行的细节——重点是大模型根据任务自主决策使用什么工具，然后由外部工具执行具体任务。\n工具执行后的内容，又会作为“记忆”保存在上下文中，方便下一轮对话中大模型进一步分析、解答用户提问。\n不难看出按照上面的框架，可以在一个Agent 中扩展多个工具——因为，只要继续追加 tools 就好了嘛。\nMCP 出世 如果有多个的Agent 都需要调用 tavily_search 这个工具，会怎样？\n没错！需要在每个Agent的tools.py代码/workflow 中都添加这个tavily_search 工具。\n如果公司里就我一个 Agent 开发人员，可能问题不大。\n但是，如果公司有10几个开发人员负责开发10个不同的Agent，一个小小的变更带来的沟通、修改 、维护成本都会很高。\n这样，MCP的需求场景就清晰了（让Agent和 Tools 间实现松耦合的结构） ：\n同一个工具函数，需要在不同地方重复调用 同一个工具函数，分散在各个项目中，修改和管理分散 如果可以将这些工具集中管理，一方面可以让使用者和执行者解耦合（分离） 另一方面，分离后还可以进一步做使用人员的权限控制，比如：这个工具开发人员A 可以用，B用不了。 而分离后，LLM 与 Tools 间通信的协议，就是我们今天的主角 —— MCP。\n没错，MCP 本质上 不是工具(Tools) 也不是 大模型新增的功能模块， 而是Agent与tools 函数间的“桥梁”——通信协议。\n该协议本着 有什么用什么的原则：\n在本地，使用STDIO 实现两个进程间通信；跨设备，使用HTTP 协议实现两个应用间的通信。\n前者，还是只能本地执行工具，无法把“分离”的优势发挥到最大。\n后者因为发展原因，又用到的了两个子协议 ：Server-Sent Events (简称：SSE) 和 Streamable。\n它们都使用 HTTP 协议，但 SSE 是基于 HTTP 的文本消息推送协议，而 Streamable 是基于 HTTP 的二进制内容传输技术。就像电子邮件（SSE）和网盘下载（Streamable）都使用 HTTP，但使用场景还是有差异的。\n从Agent的视角看 工具函数的执行方从本地转移到了MCP server 上，详见下图 3.1-3.4 部分：\nMCP server 中执行工具的方法有多种：\n可以在mcp server 上执行本地函数 也可以在mcp server 上调用第三方服务的API接口 也可以在mcp server 上请求大模型 这里MCP协议的作用有两个 在Agent 初始化阶段，MCP server 提供工具和相关的描述信息——称为 \u0026ldquo;tools/list\u0026rdquo;\n例如：\n下图记录了MCP client 与MCP server 间成功建立连接后，通过 \u0026ldquo;ListTools\u0026rdquo; 获取到的四个工具（get_joblist_by_expect_job, get_job_by_resume, get_word_by_filepath, fix_resume）和每个工具的描述信息 ：\n在Agent 接收LLM返回的工具调用信息后，由MCP Client 发起工具调用——称为 \u0026ldquo;tools/call\u0026rdquo;\n从图中可以看到：MCP Client向MCP Server的工具\u0026quot;get_joblist_by_expect\u0026quot;发送如下参数——\n1 {\u0026#34;job\u0026#34;: \u0026#34;AI应用工程师\u0026#34;} MCP server 的实现 好了，MCP出现的背景和工作流程我们讲清楚了。下面重点放在MCP server 的实现上 。\n这里以一个“求职助手” MCP Server为例：从 0-1 实现这个功能。\n该助手可以帮助求职者在职位筛选和简历修改上，使用大模型分析、识别与求职者匹配度高的岗位，并输出优化后的个人简历。\n该MCP Server 主要提供以下四个工具：\n工具名 工具描述 工具参数 调用LLM get_joblist_by_expect_job 根据求职者的期望岗位获取岗位列表数据 job: [职位名] N get_job_by_resume 根据岗位列表以及求职者的简历获取适合求职者的三个岗位及提供相关求职建议 jobs: [职位清单], resume: [个人简历] Y get_word_by_filepath 读取指定路径的word文件 filepath: [word 简历路径] N fix_resume 根据目标岗位要求改写并完善个人简历 jd: [目标职位jd], resume: [个人简历] Y 目标是帮助求职者筛选出匹配度高的职位并输出优化后的简历，关系图如下——\n0. LLM Client \u0026amp; 提示词准备 如上文所说 \u0026ldquo;get_job_by_resume\u0026rdquo; 和 \u0026ldquo;fix_resume\u0026rdquo; 这两个工具是需要调用 LLM完成简历内容处理的，所以需要准备一个LLM 调用模块—— \u0026ldquo;LLMClient\u0026rdquo;。\n模块中，提前准备了 deepseek-chat 模型的接口 和 API 参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import os import logging from openai import OpenAI from dotenv import load_dotenv class LLMClient: def __init__(self, logger: logging.Logger): self.logger = logger self.client = self._get_client() def _get_client(self)-\u0026gt;OpenAI: load_dotenv() # 根据自己环境，可以选择不同的模型提供方 client = OpenAI( api_key=os.getenv(\u0026#34;DEEPSEEK_API_KEY\u0026#34;), base_url=\u0026#34;https://api.deepseek.com\u0026#34;, # api_key=os.environ.get(\u0026#34;GEMINI_API_KEY\u0026#34;), # base_url=\u0026#34;http://ollama.host:8045/v1\u0026#34; ) return client def send_messages(self, messages): # 根据不同的模型提供方，选择具体的模型 response = self.client.chat.completions.create( model=\u0026#34;deepseek-chat\u0026#34;, # model=\u0026#34;gemini-3-flash\u0026#34;, messages=messages, ) return response 和之前ReACT 案例一样，这里的提示词模板也使用了占位符——{resume} {job_list} {input}\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Job_Search_Prompt = \u0026#34;\u0026#34;\u0026#34; 【AI求职助手】 你是一个AI求职助手, 我正在寻找与我的技能和经验相匹配的工作机会。以下是我的简历摘要和搜集到的岗位需求列表 【个人简历】 {resume} 【岗位需求列表】 {job_list} 请帮我匹配最合适的3个岗位, 并根据我的简历提供简要的求职建议。 \u0026#34;\u0026#34;\u0026#34; ResumePrompt = \u0026#34;\u0026#34;\u0026#34; 你是一个 AI 简历助手。我会给你提供我的简历以及某公司的详细岗位要求。你的任务是根据公司的岗位要求, 帮我改写和完善我的简历，使我的简历符合该公司的要求。 此外，我还会给你一个简历模板，模板中会包含简历中部分内容的大纲，当你匹配到我的简历中有模板提及的内容时，要按照我模板的格式进行编写。 简历： {resume} 简历模板： 专业技能 请在此描述符合职位要求的技能，尤其是AI方向的技能 项目经验 (1) 项目描述 (2) 我在项目中的角色 (3) 项目规模 (4) 技术堆栈 (5) 已开发模块的描述 (6) 解决难题的经验 岗位要求： {input} \u0026#34;\u0026#34;\u0026#34; 1. 本地工具 这里以后两个工具（get_word_by_filepath、fix_resume）为例——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class JobTools(LLMClient): def register_tools(self, mcp: Any): \u0026#34;\u0026#34;\u0026#34;Register job tools.\u0026#34;\u0026#34;\u0026#34; @mcp.tool(description=\u0026#34;读取指定路径的word文件\u0026#34;) def get_word_by_filepath(filepath: str) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34;根据文件路径获取word文件内容\u0026#34;\u0026#34;\u0026#34; content=read_word_file(filepath) return content @mcp.tool(description=\u0026#34;根据目标岗位要求改写并完善个人简历\u0026#34;) def fix_resume(jd: str, resume: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;根据目标岗位要求改写并完善个人简历\u0026#34;\u0026#34;\u0026#34; #将待优化简历以及目标岗位jd信息注入到 prompt 模板 prompt = ResumePrompt.format(resume=resume,input=jd) messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt}] self.logger.info(f\u0026#34;prompt: {prompt}\u0026#34;) #发送给 LLM response = LLMClient.send_messages(self,messages) response_text = response.choices[0].message.content return response_text 不难看出：MCP server 中的工具和开头举例的 \u0026ldquo;def tavily_search(query: str) \u0026quot; 工具一样——还是函数。\n只是，它被装饰器 (@mcp.tool) 装饰过后，它多了一些mcp tool所需的元数据（工具名、工具描述、工具参数）\n另外，fix_resume 这个工具在执行过程中需要调用 LLM，所以工具中进行了模型消息拼装——\n1 2 3 4 5 prompt = ResumePrompt.format(resume=resume,input=jd) messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt}] #拼装好后 发给上一步中 准备好的LLMClient response = LLMClient.send_messages(self,messages) 2. MCP Server 封装实现 最后，使用mcp中的FastMCP 帮助我们快速实现 MCP server 的组装。\n从外到里看包含：http入口、http安全配置、认证中间件、server sse初始化、工具注册。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 import os import logging import argparse from mcp.server.fastmcp import FastMCP from mcp.server.transport_security import TransportSecuritySettings from .tools.job import JobTools import uvicorn from starlette.responses import JSONResponse class JobSearchMCPServer: def __init__(self): self.name = \u0026#34;jobsearch_mcp_server\u0026#34; # 配置传输安全设置，允许本地主机和自定义域名 transport_security = TransportSecuritySettings( enable_dns_rebinding_protection=False, # 禁用 DNS rebinding protection 以支持动态域名 allowed_hosts=[ \u0026#34;127.0.0.1\u0026#34;, \u0026#34;localhost\u0026#34;, # 本机局域网ip \u0026#34;192.168.*.*\u0026#34; ] ) # 初始化 FastMCP self.mcp = FastMCP( \u0026#34;StatelessServer\u0026#34;, stateless_http=False, transport_security=transport_security ) # 开启日志 logging.basicConfig( level=logging.INFO, format=\u0026#34;%(asctime)s - %(name)s - %(levelname)s - %(message)s\u0026#34;, ) self.logger = logging.getLogger(self.name) # 调用工具注册函数 self._register_tools() def _register_tools(self): \u0026#34;\u0026#34;\u0026#34;Register all MCP tools.\u0026#34;\u0026#34;\u0026#34; job_tools = JobTools(self.logger) job_tools.register_tools(self.mcp) def get_app(self, host: str, port: int): \u0026#34;\u0026#34;\u0026#34;Get the ASGI application for SSE-based MCP server.\u0026#34;\u0026#34;\u0026#34; self.logger.info( f\u0026#34;Starting MCP Server in SSE-based mode on {host}:{port}\u0026#34; ) self.logger.info(f\u0026#34;MCP Server is accessible at http://{host}:{port}\u0026#34;) # 使用 sse 模式对外提供 MCP 服务 app = self.mcp.sse_app() # 使用 ASGI 包装器直接实现认证，避免 BaseHTTPMiddleware 处理流式响应的问题 async def auth_wrapper(scope, receive, send): mcp_token = os.getenv(\u0026#34;MCP_AUTH_TOKEN\u0026#34;) if scope[\u0026#34;type\u0026#34;] == \u0026#34;http\u0026#34;: path = scope.get(\u0026#39;path\u0026#39;, \u0026#39;\u0026#39;) method = scope.get(\u0026#39;method\u0026#39;, \u0026#39;\u0026#39;) # 添加详细日志，观察到底是哪个路径在报错 self.logger.info(f\u0026#34;Incoming request: {method} {path}\u0026#34;) # 获取请求头 auth_header = None for key, value in scope.get(\u0026#34;headers\u0026#34;, []): if key.lower() == b\u0026#34;authorization\u0026#34;: auth_header = value.decode(\u0026#34;utf-8\u0026#34;) break # 验证逻辑 if not auth_header or not auth_header.startswith(\u0026#34;Bearer \u0026#34;): self.logger.warning(f\u0026#34;Unauthorized: {method} {path}\u0026#34;) response = JSONResponse({\u0026#34;error\u0026#34;: \u0026#34;Unauthorized\u0026#34;}, status_code=401) await response(scope, receive, send) return # 校验 Token try: token = auth_header.split(\u0026#34; \u0026#34;)[1] if token != str(mcp_token): raise ValueError(\u0026#34;Token mismatch\u0026#34;) except Exception: response = JSONResponse({\u0026#34;error\u0026#34;: \u0026#34;Invalid Token\u0026#34;}, status_code=401) await response(scope, receive, send) return # 认证通过，交给 FastMCP 的 SSE App 处理 await app(scope, receive, send) # 最终，返回 ASGI 应用 return auth_wrapper def main(): parser = argparse.ArgumentParser(description=\u0026#34;Run MCP SSE-based server\u0026#34;) parser.add_argument(\u0026#34;--host\u0026#34;, default=\u0026#34;0.0.0.0\u0026#34;, help=\u0026#34;Host to bind to\u0026#34;) parser.add_argument(\u0026#34;--port\u0026#34;, type=int, default=8000, help=\u0026#34;Port to listen on\u0026#34;) args = parser.parse_args() server = JobSearchMCPServer() app = server.get_app(host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port) 身份认证 token 写在环境 变量MCP_AUTH_TOKEN 中——\n让AI 画个逻辑图，更容易理解一些——\n1 2 3 4 5 6 7 8 9 10 请求进来 (任何 path) ↓ auth_wrapper (ASGI 中间件) ├─ 没带 Authorization → 401 {\u0026#34;error\u0026#34;: \u0026#34;Unauthorized\u0026#34;} ├─ Token 不匹配 → 401 {\u0026#34;error\u0026#34;: \u0026#34;Invalid Token\u0026#34;} └─ Token 正确 → 放行 ↓ FastMCP.sse_app() (MCP 协议处理) ↓ JobTools 注册的所有工具 至此整个 MCP server 就搭建完了。\n最终项目结构长这样——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 jobsearch_mcp_server/ ├── .env ├── pyproject.toml ├── src/ │ └── jobsearch_mcp_server/ │ ├── job.txt │ ├── __init__.py │ ├── server.py # 主服务 │ ├── llm/ │ │ └── llm.py │ ├── prompt/ │ │ └── prompt.py │ ├── tools/ │ │ └── job.py │ └── word/ │ ├── word.py │ └── 个人简历.docx 使用 uv 工具启动项目——\n1 2 3 4 # run mcp server pip install uv uv sync uv --directory S:\\trae_pj\\jobsearch_mcp_server-1.0.0\\src\\jobsearch_mcp_server run jobsearch-mcp-server 调用效果 因为本文重点在MCP server 的实现，Agent部分的差异和设计不表。\n测试时还是请出老朋友 chatbox。\n支持 MCP Client的工具都可以。例如：Trace、Cursor、Roo、Claude Code。。。\n1. MCP Client 配置 Client 中的配置内容主要就是： mcp server 的http 地址、接口和认证凭据。\n1 2 3 4 5 6 7 8 9 10 11 12 {\u0026#34;mcpServers\u0026#34;: { \u0026#34;remote_jobsearch\u0026#34;: { \u0026#34;url\u0026#34;:\u0026#34;http://192.168.202.100:8000/sse\u0026#34;, \u0026#34;headers\u0026#34;: { \u0026#34;Authorization\u0026#34;:\u0026#34;Bearer key_567890\u0026#34; } }, } } 配置上是不是很简单？\n如上文提到的：4个具体工具的信息是通过sse 协议 list_tools 方法自动获取到的。\n2. 用户提问 在chatbox 中，新建个对话，并启用新添加的MCP server ：\n提问\u0026quot;AI工程师的职位有哪些\u0026rdquo;——\n然后，让Agent 基于自己的简历，按照“AI应用工程师”的岗位要求改写——\n在完成这个任务的过程，并没有定义如何看文件，看了文件之后又怎么比对“AI 应用工程师”的JD，以及简历又该朝哪个方向修改。\n都是 LLM通过提示词中用户问题和tools中的 function calling ，自主分析判断的。\n这也是 Agent 模式解决问题的长处。\n简历改写后的内容——\n补充 MCP工具提供的内容在Agent中的位置 提供function calling 的参数 工具执行的结果，会作为Agent \u0026ldquo;contents\u0026rdquo; 的一部分“记忆”下来\nMCP调用其他资源 MCP 协议除了可以调用\u0026quot;Tools\u0026rdquo; 这一种资源外 ，其实还可以调用文件和提示词模板 。\n只是用得很少，同样也需要Agent中扩展支持。\n简单了解即可——\n小结一下 本文大致梳理了MCP 出现的背景和意义：\n其核心是定义了Agent 与工具之间的通用通信协议。实现了：\n松耦合结构 优化了多工具维护和复用的通点 当然再往后推进就是 去年底开始火出圈的 Skills\n有机会 我们再聊一聊Skills\n","date":"2025-12-30T10:52:44+08:00","image":"https://r2.blog.nxlan.cn/PicGotitle_img.png","permalink":"https://blog.cba.nxlan.cn/p/mcp_server_example/","title":"一文读懂 MCP 协议与求职助手案例实战"},{"content":"想说点啥 这篇文章构思有半个多月了，本来想弄个文生图的案例分享一下。\n结果，最近在补 MCP 和 RAG的高级用法，还有 LangGraph。耽搁了一阵。\n另一方面，也没想好这里的主干内容怎么写：继续分享怎么创建工作流么？好像已经写过不少案例了。\n直到参考网上资料手搓了个Agent代码出来——感觉这里面很有意思，值得说道说道。\n所以，本文会从一个游戏案例出发 ，带你了解：\n1）LLM + Tools的调用细节和代码级实现\n2）多模态大模型在文生图案例中的效果和提示词\n3）最简单的多Agent 协同方式\n4）[重点] 什么是ReAct 以及 ReAct这个范式的优缺点\n游戏玩法和效果 游戏玩法 玩法很简单：用户输入一个卡通人物特点的简单描述，由工作流生成对应人物画像。\n游戏组件 整个工作流展示：\n主要组件就是 图中两个Agent——第一个Agent负责对卡通人物进行识别并输出人物描述，第二个Agent 则依据前面的人物描述 ，生成相关人物图像。\n效果展示 我这里输入“浪浪山的小妖怪其中的黄鼠狼，一个碎碎念的有志青年”，等待1分钟后，输出了下面这张图。\n怎么样？是不是除了脸部细节，其他还挺还原动画人物的。\n关键步骤细节 人物形象描述Agent 提示词部分如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 角色 你是一个卡通人物形象描述专家，根据用户提供的基本信息：\u0026#34;{{ $(\u0026#39;When chat message received\u0026#39;).item.json.chatInput }}\u0026#34; 输出相关人物形象描述。 如果遇到你不知道的人物信息，可以调用工具进行检索。 ## 案例 { name: “多啦爱梦” description: “一只圆润的蓝色猫型机器人。大头，白色圆脸配巨大白眼睛与红球鼻。蓝色桶状身体，中央有白色半圆口袋。红色项圈挂黄铃铛。白色短肢，末端为红色圆球手与白色圆柱脚。整体线条圆滑，色彩鲜明（蓝、白、红、黄）。” } ## 注意事项 1. 不要尝试解释和讨论人物相关信息。 2. 输出内容以指定的json格式输出。 3. 人物形象描述简明、清晰，限制在100字以内。 ​\t提示词框架上仍然是之前多次使用的框架—— “角色 - 任务 - 少量案例 - 负面提示”。\n​\t其关键部分为：提示词中的角色部分使用了用户输入变量 {question}，并特别强调可以调用工具进行信息检索——为的是补全LLM “出厂”后缺失的数据。\n小问题：这里的提示词对LLM 来说是系统提示词(System Prompt)，还是用户提示词(User Promopt) ?\n然后，LLM发现自己无法准确描述浪浪山中的动画人物（显然这个人物数据比较新）。\n于是，按照提示词中的说明，选择调用外部工具（web search）检索人物具体信息——\n先后使用 “浪浪山的小妖怪 黄鼠狼 角色 形象”和 “浪浪山小妖怪 黄鼠狼精 形象描述 外貌特征” 两条不同的检索词去查找——\n多次检索后，LLM 判断拿到了足够的数据。\n从LLM的角度看：调用工具(Tools) 的过程，类似于前端调用后端的API接口——不关心具体处理过程，最后能拿到需要的数据就可以。\n最后按照 指定的格式{ \u0026ldquo;name\u0026rdquo;: \u0026ldquo;黄鼠狼精\u0026rdquo;, \u0026ldquo;description\u0026rdquo;: \u0026ldquo;一只拟人化的黄鼠狼妖怪\u0026hellip; \u0026hellip;\u0026rdquo; } 输出本次任务执行结果。 不难看出，Agent 这里的实现过程为：\nLLM 识别用户问题 \u0026ndash;\u0026gt; 发起工具调用，尝试获取人物准确信息 \u0026ndash;\u0026gt; 尝试使用不同的查询语句，进一步查询 \u0026ndash;\u0026gt; 最后确定得到所需数据 \u0026ndash;\u0026gt; 按照要求输出 json 格式数据。\n这其中所有子动作，如：检索词调整，去哪里检索，判断检索到的内容是否有用，以及如何汇总为 100字以内的人物描述——所有这些以往需要人工操作、执行的动作，在大模型内部自主决策并执行了。\n还有一点：虽然用户没有参与具体的执行步骤。但是，可以观察到它检索了什么主题，得到了什么数据等内部细节 。\n把以上实现过程抽象一下，就是后面会重点讨论的 “ ReAct”模式: Reason + Action。\n该模式最大的好处就是：将传统 AI 那种黑盒执行的过程 暴露出来！并大幅降低 AI “一本正经胡说八道”的情况。\n\u0013\u0013\u0013\u0013\n文生图Agent 在这个Agent中，目标是实现使用上一步骤的json数据：\n​\t{ \u0026ldquo;name\u0026rdquo;: \u0026ldquo;黄鼠狼精\u0026rdquo;, \u0026ldquo;description\u0026rdquo;: \u0026ldquo;一只拟人化的黄鼠狼妖怪\u0026hellip; \u0026hellip;\u0026rdquo; }\n生成对应人物图像。\n和生图大模型的交互，还是使用提示词。那提示词是怎么写的呢？\n1 2 3 4 5 6 7 8 9 10 11 12 # 角色 你是一个文生图助手，可以调用工具 产出一副符合相关描述的图片。 # 执行步骤： 1. **生成图像:** * 接收 关于人物`{{ $json.output.name }}`相关描述`{{ $json.output.description }}` 的信息。 * 一步到位地将该情节合成一段详细的文生图描述。 * **必须在描述中加入构图指令:** **\u0026#34;主体必须被放置在画面的顶部 3/4 区域内。背景是纯白色，且画面的底部 1/4 区域必须是完全空白的纯白色，为文字预留。\u0026#34;** * 确保适配下游的 \u0026#34;mcp\u0026#34; 图片生成工具，生成960*1280比例的图片。 2. 调用mcp 图片生成工具 3. **返回结果:** * 返回编辑后的图片链接，只返回图片链接，不包含任何无关内容。 其中，上一步输出的人物信息可以直接在提示词中以变量名 \u0026ldquo;name\u0026rdquo; 和 \u0026ldquo;description\u0026quot;引用。\n同时，提示词中也明确指定了出图的尺寸( 960 * 1280 )和位置信息( 上部 3/4的区域 ) 。当然还可以指定画面风格 ，这样就可以进一步保证产出的图片风格是一致的。\n为了保持每次生成的图片一致性，以上这些细节（背景，尺寸，位置，颜色，字体，风格等）都是必要的。\n这也是调用多模态模型和自然语言模型时的主要差别。\n这里用到的生图工具，是阿里云的百炼生图模型。\n在MCP工具中，可以看到LLM在调用工具时，根据提示词 补全了相关工具参数——生成图片的张数、大小、生图提示词：\n单Agent vs 多Agent 系统 本案例，就是一个最简单的多Agent 系统。稍微提一句：什么是多 Agent ?\n我理解下来，就是有的任务变复杂了，一个Agent 完成的效果不好，拆分任务到两个（甚至更多）Agent 中，效果不错。于是就采用这种分工的方式共同完成某一项任务 。\n类比于餐厅：\n顾客不多时， 一个店员负责洗、切、炒、送、收钱。——可实现基本功能，成本低，但是出餐慢、做不了太复杂的菜。\n顾客很多时，点单收钱的一个店员，送菜擦桌子的一个店员，后面炒菜配菜 可能是两个店员。\n—— 扩展性好，每个人专注于自己的任务，上菜效率高，但是成本和复杂度也高，如果某个环节断了，其他人也无能为力。\n对比单Agent 和 多Agent间的差异：\n维度 单Agent系统 多Agent系统 核心定义 一个独立的、功能完备的AI模型或程序，负责处理从输入到输出的完整任务。 由多个相互协作、通信的AI Agent组成的系统，每个Agent有特定角色或专长，共同完成复杂任务。 优势 1. 简单性： 架构简单，易于开发、部署和调试。\n2. 一致性： 决策和行为风格统一，输出连贯。\n3. 可控性： 责任边界清晰，易于监控和管理。\n4. 资源效率： 通常计算和通信开销较低。 1. 专业化与模块化： “分而治之”，每个Agent可针对特定子任务进行优化，能力更强。\n2. 复杂问题解决： 能处理需要多步骤推理、多领域知识或并行任务的复杂工作流。\n3. 鲁棒性与容错性： 单个Agent故障不一定导致系统崩溃，任务可能由其他Agent接管或重试。\n4. 可扩展性： 可通过增加新的专业Agent来轻松扩展系统能力。 劣势 1. 能力瓶颈： 受限于单一模型的能力上限，难以精通所有领域。\n2. 单点故障： 一旦该Agent出错，整个系统即失效。\n3. 灵活性差： 工具一多的对大模型的理解、规划要求随之变高。\n4. 长对话下表现变差： 过长的对话内容，大模型容易失焦，tokens 消耗大。 1. 系统复杂性： 架构、通信协议和协作逻辑的设计与调试极其复杂。\n2. 协调开销： Agent间的通信、协商、任务分配会引入显著的延迟和计算成本。\n3. 一致性与连贯性挑战： 需精心设计以确保最终输出的整体一致性和风格统一。\n4. 开发与运维成本高： 需要更多开发资源，且监控、维护难度大。 核心挑战 1. 能力泛化： 如何让一个模型具备广泛且深入的能力。\n2. 任务分解： 在模型内部有效进行复杂的任务规划和步骤分解。 1. 高效协作机制： 如何设计通信协议（如共享黑板、消息传递）、决策框架（如投票、领导选举）以实现高效协作。\n2. 知识共享与冲突消解： 如何让Agent共享上下文，并解决它们之间可能产生的意见或行动冲突。\n3. 系统级优化： 如何优化整体工作流，减少通信轮次，避免“讨论循环”。\n4. 评估难度： 难以评估是哪个Agent或协作环节导致了最终的成功或失败。 典型应用场景 • 简单的问答与对话\n• 文本摘要/翻译\n• 基础内容生成\n• 单一工具调用（如查天气） • 复杂的项目规划与执行\n• 多步骤研究与分析报告\n• 软件开发（设计、编码、测试分工）\n• 模拟社会或经济系统 那使用哪种Agent 系统比较好呢 ?\n第一点，单Agent可以满足业务需求的，就使用单Agent。\n第二点，单Agent 效果不好时，再从以下四个维度评估应用场景：问题复杂度、工具种类 、容错性要求、问题泛性等四个方面。\n角度 案例产品 案例说明 1. 问题复杂度不高 ChatGPT / Claude 网页聊天界面 用户与一个单一的、强大的语言模型进行开放式对话。虽然模型本身复杂，但任务形态是线性的：接收提示词 -\u0026gt; 思考 -\u0026gt; 生成回复。它不需要在内部模拟多个角色进行辩论或分工。 2. 工具种类不多 Notion AI / 文档助手 这类产品深度集成在特定环境（如文档编辑器）中。它的核心工具集非常聚焦：文本生成、改写、总结、翻译。它不需要同时调用代码解释器、搜索引擎、绘图API等多种外部工具来完成一个任务。 3. 容错性要求不高 AI 写作助手 (如 文案仿写） 用于营销文案、博客草稿、广告语生成。输出结果通常需要人工审核和润色，AI提供的是创意初稿或灵感，而非最终交付物。即使偶尔生成不理想的内容，用户修正的成本较低，容错空间较大。 4. 问题泛性可约束 智能客服聊天机器人 虽然基于大模型，但其任务范围被严格约束在公司产品、服务、政策的问答上。通过系统提示词、知识库检索和对话流程设计，将AI的泛化能力引导至一个明确的业务范围内，确保回答的相关性和准确性。 单Agent 系统效果不好的场景：项目代码AI工具（例如市面上比较知名的 Cursor, Claude Code, Trae）。\n这种就不是一个Agent 可以独立完成的，它必然是多Agent 的结构。因为它不满足上述四个维度中任何一个——通用场多，逻辑和实现上复杂，调用的工具种类多，而且容错性要控制在低的水平。\n而多Agent 最大的挑战就是跨Agent 间的通信和协作。架构上和工程上都会复杂的多。\n这里以OpenManus 结构为例，有两个Agent：一个负责指定规划（不执行），一个负责具体执行和执行过程中的检查、矫正（结构上同样参考了 ReAct 模式）。两者间有一个 todo-list（代办事项）作为沟通协作的桥梁。\n代码级复现“人物形象描述Agent” 最后花点功夫，使用简单的Python 代码实现 游戏中第一个Agent——人物形象描述Agent。\n用到的工具一览 工具名 工具类型 描述 Python 面向对象的编程语言工具 \u0026gt;=3.10 版本，生成Agent代码 uv 增强版的 python 包管理工具 处理\u0026amp;记录python项目依赖包，同时生成项目相关描述文件 openai-sdk OpenAI提供的SDK包 用于生成LLM 实例 tavily-sdk Tavily 提供的SDK包 用于实现 web检索工具 DeepSeek API_key API Token 调用 DeepSeek 大模型的用户凭据 tavily API_key API Token 调用 Tavily 检索服务时的用户凭据 项目代码 因为有SDK 工具的帮助，实现起来真的不复杂。\n感兴趣的话 可以后台发送【1215】获取原代码。\n项目目录下的就4个文件：tools.py， llm.py， prompt.py， agent.py。\n他们相互间的关系：\n文件名 资源描述 调用关系 tools.py web search 工具描述\nweb search 函数 Json格式化的tools 描述 \u0026ndash;\u0026gt; 填充进prompt.py提示词{tools}中\nTavily-SDK \u0026ndash;\u0026gt; 工具函数tavily_search（）\u0026ndash;\u0026gt; agent.py 中使用 llm.py llm 客户端（调用 DeepSeek API 接口） OpenAI-SDK \u0026ndash;\u0026gt; llm.client \u0026ndash;\u0026gt; agent.py 中 send_messages() 函数 prompt.py ReAct 模式提示词（含有带替换的数据占位） prompt \u0026ndash;\u0026gt; 构成agent.py send_messages(message) 函数中message 的一部分 agent.py send_messages( ) 函数ReAct 模式的Agent实现 send_messages( ) \u0026lt;\u0026ndash; llm.client, ReAct 提示词\nAgent执行任务 \u0026lt;\u0026ndash; send_messages(message) 函数 \u0026lt;\u0026ndash; 预先定义好的系统提示词和填充{tools}和{user_input}后的用户提示词 \u0026lt;\u0026ndash; 用户提问内容, Json格式化的tools 描述 运行一下，看看效果 1 2 3 4 5 # 为了演示，用户输入在代码中预先写好了： query = \u0026#34;浪浪山的小妖怪中的黄鼠狼，一个碎碎念的有志青年\u0026#34; # 运行agent uv sync cd AI_Agent_demo\\src\\ai-agent-demo uv run python agent.py Agent 在做什么 通过输出的前缀可以暴露LLM 的执行步骤：这里大模型思考（Thought）了两次，工具执行（Action）一次。\n注意：工具执行环节不是LLM 完成的，而是由代码中的工具函数tavily_search() 执行的。\n它们间的关系是： LLM 生成函数查询语句 {\u0026ldquo;query\u0026rdquo;: \u0026ldquo;浪浪山的小妖怪 黄鼠狼 角色 形象\u0026quot;}，然后交给工具函数tavily_search(\u0026ldquo;浪浪山的小妖怪 黄鼠狼 角色 形象\u0026rdquo;)执行，执行后的结果 再放入messages中，提供给下次循环中LLM使用。\n在ReAct提示词模板中，通过文字描述定义了大模型的状态标签和状态流转过程。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 REACT_PROMPT = \u0026#34;\u0026#34;\u0026#34; You run in a loop of Thought, Action, Action Input, PAUSE, Observation. At the end of the loop you output an Answer Use Thought to describe your thoughts about the question you have been asked. Use Action to run one of the actions available to you use Action Input to indicate the input to the Action- then return PAUSE. Observation will be the result of running those actions. Your available actions are: {tools} Rules: 1- If the input is a greeting or a goodbye, respond directly in a friendly manner without using the Thought-Action loop. 2- Otherwise, follow the Thought-Action Input loop to find the best answer. 3- If you already have the answer to a part or the entire question, use your knowledge without relying on external actions. 4- If you need to execute more than one Action, do it on separate calls. 5- At the end, provide a final answer. Some examples: ### 1 Question: 今天北京天气怎么样？ Thought: 我需要调用 get_weather 工具获取天气 Action: get_weather Action Input: {\u0026#34;city\u0026#34;: \u0026#34;BeiJing\u0026#34;} PAUSE You will be called again with this: Observation: 北京的气温是0度. You then output: Final Answer: 北京的气温是0度. Begin! New input: {input}\u0026#34;\u0026#34;\u0026#34; 问题解决的过程被细化为不同状态：\n\u0026ldquo;Thought\u0026rdquo;: \u0026ldquo;问题规划\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;Action\u0026rdquo;: \u0026ldquo;外部工具名\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;Action Input\u0026rdquo;: \u0026ldquo;外部工具参数\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;PAUSE\u0026rdquo;（等待工具执行结果） \u0026ndash;\u0026gt; \u0026ldquo;Observation\u0026rdquo;: \u0026ldquo;工具执行结果\u0026rdquo; \u0026ndash;\u0026gt; \u0026ldquo;Thought\u0026rdquo;: \u0026ldquo;判断执行结果是否有助于回答用户提问\u0026rdquo; \u0026ndash;if可以解决\u0026ndash;\u0026gt; \u0026ldquo;Final Answer\u0026rdquo;: \u0026ldquo;LLM 参考工具执行结果 回答用户提问\u0026rdquo;\n逻辑上 Agent 的执行过程就是个 循环语句——直到LLM 判断得出 “Final Answer:” 后任务才会停止\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 while True: response = send_messages(messages) response_text = response.choices[0].message.content print(\u0026#34;大模型的回复：\u0026#34;) print(response_text) final_answer_match = re.search(r\u0026#39;Final Answer:\\s*(.*)\u0026#39;, response_text) if final_answer_match: final_answer = final_answer_match.group(1) print(\u0026#34;最终答案:\u0026#34;, final_answer) # LLM 答复中出现 \u0026#34;Final Answer:\u0026#34; 才会跳出循环 break messages.append(response.choices[0].message) action_match = re.search(r\u0026#39;Action:\\s*(\\w+)\u0026#39;, response_text) action_input_match = re.search(r\u0026#39;Action Input:\\s*({.*?}|\u0026#34;.*?\u0026#34;)\u0026#39;, response_text, re.DOTALL) # LLM 思考后同时输出\u0026#34;Action\u0026#34; 和 \u0026#34;Action Input\u0026#34; 才会执行对应的工具（任务） if action_match and action_input_match: tool_name = action_match.group(1) params = json.loads(action_input_match.group(1)) observation = \u0026#34;\u0026#34; if tool_name == \u0026#34;tavily_search\u0026#34;: observation = tavily_search(params[\u0026#39;query\u0026#39;]) print(\u0026#34;工具执行：Observation:\u0026#34;, observation) else: observation = f\u0026#34;未知的工具: {tool_name}\u0026#34; print(\u0026#34;工具执行：Observation:\u0026#34;, observation) messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;Observation: {observation}\u0026#34;}) 大模型本身的思考、工具描述和工具函数执行的结果都会追加到变量 messages 中。\n1 2 messages.append(response.choices[0].message) messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;Observation: {observation}\u0026#34;}) 再考虑到Agent本身就是循环执行的，可以想见如果一个问题要多次调用一个或多个工具，messages中的内容也是越来越长的。\n信息越来越长的结果有几个：\nTokens 消耗加速 长的上下文可能会导致模型失焦 过长的上下文（如 16k） 可能达到LLM 上下文的上限，那超出部分的内容模型根本看不到 ReAct 范式是什么 首先 他不是单独一个元素：不是工具，不是agent ，也不是提示词。\n它是一套指导AI解决问题的框架：通过暴露解决问题的状态和灵活调用外部工具的特点，从逻辑和数据两个层面 提高AI回答问题的能力。在回答质量和回答准确度上都有提升。\n可以说 ，这就是当前单Agent系统下的标准框架。\n【彩蛋】 ","date":"2025-12-09T15:21:18+08:00","image":"https://r2.blog.nxlan.cn/PicGobase_MA.png","permalink":"https://blog.cba.nxlan.cn/p/base_multiagent/","title":"基础的多Agent案例 + 深入探究ReAct范式"},{"content":"不小心吹个牛 一两个月前，我对大模型还比较迷信。觉得这模型真厉害，平时遇到啥问题，问它都可以回答个八九不离十。\n遂心向往之~\n后来也看到有UP主分享：“现在不流行训练自己的小模型了！真实项目中往往都是使用开源大模型+行业数据。”\n同事问我：“元芳，你怎么看？”\n我回答他，“耳听为虚，眼见为实。我要微调个自己的小模型看看。”\n然后，就拖到了现在。。。\n本文会分享以下内容：\n模型训练工具介绍（LLaMA-Factory） 在Transformer 架构下模型训练算法（本次使用微调 LoRA） 从0 开始，0花费地训练一套小模型出来 讨论什么场景下 更适合微调模型 效果展示 咱们老规矩，还是先上效果图——\n这个模型的用途是：用户输入任意新闻标题后，它可以进行类型标注。\n例如，下面这两个新闻：\n用户提问：“新闻分类：真相了？外媒：特朗普想在伊朗“更迭政权””\nAI 回答：“国际”\n用户提问：“新闻分类：许昕发文感谢队友？从陪练张继科到世界冠军！称霸世界的国乒！”\nAI 回答：“体育”\n这里的训练数据需要提前准备，格式上是这样的——\n可以看出：“人类输入的内容” + “AI回答的内容” 这样一问一答作为一条训练数据。\n本次训练集由300条这样的问答对组成。\n我们的预期则是：\n用户输入问题A \u0026ndash;\u0026gt; “训练后的模型” \u0026ndash;\u0026gt; 回答最合适的新闻类型（如：科技、体育、政治。。。）\n训练后的模型输出，越接近训练集的结果，就认为这个模型越符合预期。反之，效果就一般。\n后续模型测试中会看到，训练不成功的效果长什么样。\n下面介绍下 训练AI模型使用到的工具：LLaMA-Factory。\n它是模型训练工具的一种，适合初学者（无需编写代码）快速上手操作。也是一个开源项目，官方介绍如下：\n模型训练基础 如果了解并熟悉Transformer架构和 LoRA算法 或者 希望先上手微调训练，可以跳过本部分，查看后续操作步骤。\n有三点关键内容需要提前了解下：基座模型，微调方法以及训练参数。不然，后面的操作过程中会有点懵。。。\n1. 基座模型 先说 “基座”，顾名思义就是我们的训练是基于一个“底座”的。不是完完全全从0 开始训练一个新模型。\n因为训练这个模型的目的只是希望加强它在某一领域的知识和能力，不是取代现有大模型的通用能力。\n所以，这个基座模型一般会选择开源模型中效果和开销比较平衡的。\n比如这里使用的 DeepSeek-R1-Distill-Qwen-7B ，实测效果比 小规模的DeepSeek-R1-Distill-Qwen-1.5B 好不少。\nQ：关于这个名字，怎么既有 DeepSeek又有 Qwen？这么长一串到底啥意思？\nA： 这里涉及到开源大模型的命名规范： 机构名/产品系列+技术和知识来源+规模。以“DeepSeek-R1-Distill-Qwen-7B” 为例。分为三个部分：\nDeepSeek-R1：DeepSeek 下的推理模型（慢思考，善于处理复杂逻辑内容） Distill-Qwen：通过蒸馏(distillation) 通义干问 Qwen2.5 的通用知识 7B：模型参数量70 亿 它结合了 DeepSeek-R1的推理能力 + qwen 的通用知识，资源消耗也不大。\n这里我们只关心，相关“行业数据”（新闻）的输出效果。而其他方面的能力在训练后可能会变弱，例如计算能力，写作能力等。\n2. 微调方法（LoRA） 这块涉及到 Transformer 架构下的数学矩阵计算：\n1）模型参数被转化为了数字向量\n2）向量的值是一个范围巨大的矩阵。为了方便理解这里以 100*100 的矩阵（全秩矩阵），表示原先的 模型参数量。\n3）问题来了：如果按照传统的训练微调（全参数微调）方式，就要训练全秩矩阵中每一个(10000)的值，工程量大 控制起来也复杂。\n​ 有人就想出个优化的办法：通过两个小矩阵相乘 [ 100 * 2 ] * [ 2 *100 ] 可以填充全秩矩阵（100 * 100）的每个单元。\n所以，LoRA（Low-Rank Adaptation of Large Language Models）方法就是精简训练上图 A B两个小矩阵。\n与LoRA 类似的训练小参方法，还有 QLoRA、Adapter、P-Tuning。简单了解一下——\n3. 训练参数 先看一眼基本的训练参数：\n常用来调整的参数 加粗表示下。\n训练阶段（stage）：常用 SFT（Supervised Fine-Tuning）有监督微调，不常用 DPO（Direct Preference Optimization）和 PPO（Proximal Policy Optimization） 学习率（Learning Rate）：决定了模型每次更新时权重改变的幅度。过大可能会错过最优解；过小会学得很慢或陷入局部最优解 训练轮数（Epochs）：太少模型会欠拟合（没学好），太大会过拟合（学过头了） 最大梯度范数（Max Gradient Norm）：当梯度的值超过这个范围时会被截断，防止梯度爆炸现象 最大样本数（Max Samples）：每轮训练中最多使用的样本数 计算类型（Computation Type）：在训练时使用的数据类型，常见的有 float32 和float16。在性能和精度之间找平衡 截断长度（Truncation Length）：处理长文本时如果太长超过这个阈值的部分会被截断掉，避免内存溢出 批处理大小（Batch Size）：由于内存限制，每轮训练我们要将训练集数据分批次送进去，这个批次大小就是 Batch Size 梯度累积（Gradient Accumulation）：默认情况下模型会在每个 batch 处理完后进行一次更新一个参数，但你可以通过设置这个梯度累计，让他直到处理完多个小批次的数据后才进行一次更新 验证集比例（Validation Set Proportion）：数据集分为训练集和验证集两个部分，训练集用来学习训练，验证集用来验证学习效果如何 学习率调节器（Learning Rate Scheduler）：在训练的过程中帮你自动调整优化学习率页面上点击启动训练，或复制命令到终端启动训练 好了，铺垫了这么多。终于可以动手操作了。\n模型微调实操 下面就是从0 开始，一步一步 微调我们需要的模型。\n相关测试数据和训练后的模型，我放在公众号 【AI 热气球】中，感兴趣的话可以回复1117 获取。\n0. 环境准备 既然是训练自然少不了显卡的加速，通常有三种方式：使用大模型提供商的在线微调服务、租用云平台的机器、本地自集成部署。\n数据安全方面考虑： 本地自集成部署 \u0026gt; 租用云平台的机器 \u0026gt; 大模型提供商的在线微调服务\n成本开销方面考虑： 本地自集成部署 \u0026gt; 租用云平台的机器 \u0026gt; 大模型提供商的在线微调服务\n这么一看，优先选择“租用云平台的机器”完成小模型的训练和微调。毕竟我们这里数据不多，一次训练大约1小时左右。\n而且，用到的平台“魔塔” 为新用户提供36小时 带24g显卡云主机的使用优惠，还是很划算的。\n申请并创建个人魔塔账号(https://modelscope.cn/home)，过程中需要登录阿里云帐号。\n在“我的 Notebook” (https://modelscope.cn/home) 中启动GPU主机—— 版本选择 Ubuntu 22.04 的就行。\n点击“查看 Notebook”，进入控制台界面后，点击 “插件”按钮搜索并安装中文语言包。\n检查显卡工作正常\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 检查显卡驱动和状态—— root@dsw-1471676-9bf84dfcb-nmxqn: nvidia-smi Fri Nov 14 16:06:26 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 550.127.08 Driver Version: 550.127.08 CUDA Version: 12.4 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA A10 Off | 00000000:00:09.0 Off | Off | | 0% 26C P8 9W / 150W | 4MiB / 24564MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | No running processes found | +-----------------------------------------------------------------------------------------+ root@dsw-1471676-9bf84dfcb-nmxqn:/mnt/workspace# 1. llama-factory 工具安装 因为是在云平台上训练模型，云主机关机后除了个人工作目录 /mnt/workspace 下的数据都会被清空、还原。\n所以，后续相关操作（安装包下载 测试数据上传 训练模型合并、下载）都是在 /mnt/workspace 这个目录下 。\n另外，也为了llama-factory 环境运行依赖的 python 包之间不会冲突。一般建议使用conda 创建虚拟环境。\n我们这里 使用更轻量、便捷的社区版 “Miniforge”创建虚拟环境 。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 进入工作空间 cd /mnt/workspace # 下载最新的 Miniforge 安装包 wget https://github.com/conda-forge/miniforge/releases/download/25.3.1-0/Miniforge3-25.3.1-0-Linux-x86_64.sh # 执行安装 bash Miniforge3-25.3.1-0-Linux-x86_64.sh # 安装过程中，提示选择安装路径，默认为 /root/minigorge3。修改成以下路径： /mnt/workspace/miniforge3 # 启用 Miniforge 配置和mamba 工具 eval \u0026#34;$(/mnt/workspace/miniforge3/bin/conda shell.bash hook)\u0026#34; eval \u0026#34;$(mamba shell hook --shell bash)\u0026#34; # 创建名为“llama-factory”的虚拟环境： mamba create -n llama-factory python=3.10 （如果下载慢 就多试几次） # 激活虚拟环境 mamba activate llama-factory 虚拟环境准备好后，正式进入 llama-factory 安装环节： 1 2 3 4 5 6 7 8 9 # 克隆项目 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git # 进入项目 cd LLaMA-Factory # 安装项目相关依赖 pip install -r requirements.txt （第一次安装时间较长，大约30分钟） # 执行安装 pip install -e \u0026#34;.[torch,metrics]\u0026#34; 上述步骤完成，llama-factory 工具就安装好了。先查看下当期版本，后启动web界面 1 2 llamafactory-cli version llamafactory-cli webui 启动成功后，VSCode 会提示 打开一个http页面。\n该页面映射刚启动的llama-factory web 服务（http://127.0.0.1:7860）。\n打开网页，看到如下页面，表示 llama-factory 已经启动成功了。 2. 基座模型下载 这一步中，需要下载用到的基座模型 DeepSeek-R1-Distill-Qwen-7B 。\n具体操作如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 # 同样需要把模型保存在工作目录 /mnt/workspace 下 mkdir /mnt/workspace/Hugging-Face # 配置huggingface-hub 下载加速和路径 export HF_ENDPOINT=https://hf-mirror.com export HF_HOME=/mnt/workspace/Hugging-Face export HF_HUB_DOWNLOAD_TIMEOUT=30 # huggingface_hub\u0026lt;1.0 时执行下载 # 测试的话可以试试这个小模型 huggingface-cli download --resume-download deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B # 推荐使用7B这个模型 huggingface-cli download --resume-download deepseek-ai/DeepSeek-R1-Distill-Qwen-7B 如果模型下载速度太慢的话：参考这篇文章\u0026lt;如何快速下载huggingface模型——全方法总结\u0026gt;\nhttps://zhuanlan.zhihu.com/p/663712983?s_r=0\n下载完成后， 在指定的目录 /mnt/workspace/Hugging-Face 中可以看到多出一个目录 叫“models\u0026ndash;deepseek-ai\u0026ndash;DeepSeek-R1-Distill-Qwen-7B”这个就是刚才下载完成的模型。\n然后，复制模型路径，稍候启动参数时需要指明模型具体位置。\n1 2 3 4 5 # 复制7b模型位置路径 /mnt/workspace/Hugging-Face/hub/models--deepseek-ai--DeepSeek-R1-Distill-Qwen-7B/snapshots/916b56a44061fd5cd7d6a8fb632557ed4f724f60 # 如果下载了1.5b的模型，路径就是这样的 /mnt/workspace/Hugging-Face/hub/models--deepseek-ai--DeepSeek-R1-Distill-Qwen-1.5B/snapshots/ad9f0ae0864d7fbcd1cd905e3c6c5b069cc8b562 3. 基座模型运行测试 先测试当前下载的模型加载、执行是否正常：\n在已经打开的llama-factory 页面中，语言切换至“zh”，指定“模型名称”和“模型路径”。\n然后点击“Chat” \u0026ndash; \u0026gt; “加载模型”，提示加载成功后。输入用户 input，查看模型响应：\n如上图，表示模型正常响应，可以继续操作。\n4. 上传训练数据 训练数据格式有展示过，这里有两个数据文件+配置文件需要上传到 云主机上。\n其中 train.json 为训练用数据，eval.json 为评估数据。dataset_info.json 为相关配置文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # dataset_info.json 配置文件内容： { \u0026#34;train\u0026#34;: { \u0026#34;file_name\u0026#34;: \u0026#34;train.json\u0026#34;, \u0026#34;formatting\u0026#34;: \u0026#34;sharegpt\u0026#34; }, \u0026#34;eval\u0026#34;: { \u0026#34;file_name\u0026#34;: \u0026#34;eval.json\u0026#34;, \u0026#34;formatting\u0026#34;: \u0026#34;sharegpt\u0026#34; } } # 上传这三个文件到以下目录 /mnt/workspace/LLaMA-Factory/data/example_newsdata 5. 微调模型 模型准备好了，训练数据也有了。\u0026quot;微调方法\u0026quot; 默认就是 lora。这里就可以开始调整微调参数了——\n点击“Train” 界面，“训练阶段”中默认是SFT（有监督微调）不用修改。\n“数据路径”和 “数据集”选择上一步中的训练数据集“train”。\n修改微调相关参数，如：“学习率” -\u0026gt; 5e-4, \u0026ldquo;训练轮数\u0026rdquo; -\u0026gt; 10，“梯度累积” -\u0026gt; 4, “LoRA+ 学习率比例” -\u0026gt; 16, \u0026ldquo;LoRA 作用模块\u0026rdquo; -\u0026gt; \u0026ldquo;all\u0026rdquo;。\n点击“开始”按钮，llama-factory 会加载模型和训练数据 进行训练。\n进行过程中，会有一副关于训练损失和训练步数的关系图。\n不同训练参数和模型，其训练时长不等。这里 20轮训练下来 差不多25分钟左右。\n训练期间 云主机 GPU 发力工作：\n最终训练好的模型，输出在上面的“输出目录”中。后续可以基于此次训练结果，进行评估和测试。 6. 人工测试 回到 “Chat”界面，加载刚训练得到的模型检查点“train_2025-11-14-17-23-08”。\n手动测试下新模型的效果——\n测试两三次，感觉起来好像还行。模型微调的过程就告一段落了。\n但到底新模型效果如何，还是需要数据说话——\n新模型评估 我们之前不是准备了 评估数据集 “eval.json” 么，此时派上用场。\n点击“Evaluate\u0026amp; Predict” 界面，“数据路径”和 “数据集”选择之前上传的数据集“eval”。 其他评估参数保持默认，点击“开始”按钮。 完成后，注意其中 “predict_rouge-1”的分数。这个分值越高，表明新模型生成质量越好。 上图中该值为 53.85——说明新模型生成的文本与评估数据（eval.json）在单词级别上有 53.85%的重合。\n新模型导出 此时新训练出来的模型还在云主机上。我们怎么把它拖到本地呢？\n1. 首先导出模型 点击“Export” 界面，“导出设备” -\u0026gt; auto，在“导出目录”中输入模型导出路径后，点击“开始导出”。\n看到“模型导出完成”，代表成功导出了此次训练出来的新模型。\n在云主机 VSCode界面中，也可以看到新模型有哪些文件组成——\n但是，这些模型文件不能直接在本地使用 Ollama 调用。\n我们需要用到第二个开源工具——llama.cpp。\n2. 使用llama.cpp 工具 使用 llama.cpp 工具，可以将上述模型文件转为 一个 .gguf 格式的文件。\n而后者，是可以在ollama 平台中加载、运行的。\n与传统基于 Python 的 AI 框架（如 PyTorch/TensorFlow）不同，llama.cpp 选择回归底层语言C/C++的实现策略。这种设计使其摆脱了 Python 解释器、CUDA 驱动等重型依赖，通过静态编译生成单一可执行文件，在资源受限环境中展现出独特优势。\n这里，同样在云主机中完成转换工作——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 在Vscode中新建一个会话终端，并进入工作目录 cd /mnt/workspace # 克隆项目文件 git clone --depth 1 https://github.com/ggerganov/llama.cpp.git eval \u0026#34;$(/mnt/workspace/miniforge3/bin/conda shell.bash hook)\u0026#34; # [Option] 创建名为llama.cpp的新虚拟环境 mamba create -n llama.cpp python=3.10 eval \u0026#34;$(mamba shell hook --shell bash)\u0026#34; mamba activate llama.cpp # 安装项目所需依赖包 pip install -r llama.cpp/requirements.txt # 最后执行格式转换 python3 llama.cpp/convert_hf_to_gguf.py ./export_models/deepseek-r1-1.5b-merged --outfile ./dsr1_1.5b_newstype.gguf 格式转换成功——\n1 2 3 4 5 INFO:gguf.gguf_writer:Writing the following files: INFO:gguf.gguf_writer:dsr1_1.5b_newstype.gguf: n_tensors = 339, total_size = 3.6G Writing: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3.55G/3.55G [00:56\u0026lt;00:00, 63.3Mbyte/s] INFO:hf-to-gguf:Model successfully exported to dsr1_1.5b_newstype.gguf ollama 上运行新模型 下载 dsr1_1.5b_newstype.gguf 文件到本地目录，如 D:\\download\\export_models 中。\n然后，创建一个名为 ModelFile 的配置文件。注意，该文件没有后缀名。\n​\t文件中内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 FROM ./dsr1_1.5b_newstype.gguf # set the temperature to 0.7 [higher is more creative, lower is more coherent] PARAMETER temperature 0.7 PARAMETER top_p 0.8 PARAMETER repeat_penalty 1.05 TEMPLATE \u0026#34;\u0026#34;\u0026#34;{{ if .System }}\u0026lt;|im_start|\u0026gt;system {{ .System }}\u0026lt;|im_end|\u0026gt; {{ end }}{{ if .Prompt }}\u0026lt;|im_start|\u0026gt;user {{ .Prompt }}\u0026lt;|im_end|\u0026gt; {{ end }}\u0026lt;|im_start|\u0026gt;assistant {{ .Response }}\u0026lt;|im_end|\u0026gt;\u0026#34;\u0026#34;\u0026#34; # set the system message SYSTEM \u0026#34;\u0026#34;\u0026#34; You are a news category assistant. \u0026#34;\u0026#34;\u0026#34; 确认本机 ollama 服务正在运行。执行以下命令完成 新模型的导入—— 1 ollama create deepseek-r1_1.5b_newstype --file \u0026#34;D:\\download\\export_models\\ModelFile\u0026#34; 导入成功： 后续会很方便在 AI 应用中直接调用这个训练好的模型。 如今，为何不建议自己训练模型 我想有两方面原因：\n现有模型80%~90% 是基于transformer架构的生成式AI，该架构下的AI响应结果严重依赖训练数据，因为模型自己是不会推理的。\n而有用的小模型，需要行业大量的数据。如果数据不够，真实上线时，会放心交给模型自己去猜？\n所以，在数据量不足的前提下，花时间、金钱去训练自己的模型是在找死。\n但是，反过来说，如果自己的数据量充足，这个训练就很有意义了——一定场景下可以实现低成本和快速响应！\n通用大模型的能力在快速迭代，他们获取数据的广度和成本不是 中小公司可以匹敌的。两者的维度不一样，存在跨维打击的可能。\n每人希望看到：今天花了100W得到的模型 视为珍宝，明天就被通用模型超越变成黄花的局面。\n除非，自己领域的数据市面上没有第二家。\n所以，本质上要看行业数据的情况而定，如果样本足够多，样本足够稳定，才可以考虑微调训练自己的模型。\n另外，在关于 微调和 RAG的对比上——\n对比维度 模型微调 RAG 核心逻辑 让模型学会新知识或技能，改变其内部参数 为模型提供外部知识，利用其现有能力进行回答 时间成本 长（数据准备、训练、迭代） 短（主要工作是知识库构建） 资金成本 高（训练计算、评估、迭代成本） 低（主要是构建和检索成本） 适用场景 改变模型行为、学习隐性知识（如风格、格式、复杂推理） 查询动态、具体的事实性知识，要求信息溯源 数据需求 需要高质量、大规模的标注数据集，力求覆盖所有场景 按需提供，需要什么知识就准备什么文档 技术门槛 高（需机器学习/深度学习专业知识） 相对较低（更多是工程和数据处理） 迭代与维护 困难（更新知识需重新训练或增量训练） 简单（直接增删改知识库文档即可） 实时性 差，知识固化在模型中，无法感知新信息 好，知识库更新后立即可用 响应速度 生成速度快（推理阶段无需额外检索） 整体响应较慢（需\u0026quot;检索+生成\u0026quot;两个步骤） 可解释性/溯源 差，是\u0026quot;黑盒\u0026quot;，无法确认答案来源 好，可以提供引用的原始文档片段 \u0026ldquo;幻觉\u0026quot;问题 可能基于错误学习产生幻觉 能有效减少幻觉，答案基于提供的事实 =============================================================\n最后，推荐一个学习资源——来自极客时间的 课程 \u0026lt;DeepSeek 应用开发实战\u0026gt;。\n本文一部分内容，也是参考了这门课程。我自己目前也收益良多。\n课程章节感兴趣的朋友，可以免费试看这门课程中任意4个章节内容。\n","date":"2025-11-16T23:45:11+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-1763403766825-gcre34qo.png","permalink":"https://blog.cba.nxlan.cn/p/ai_training_lora/","title":"吹过的牛皮要实现 —— 速通小模型微调"},{"content":"起因 我有一个朋友，想要学习AI 方面的案例。但是苦于手头没有Linux server，就问我能不能在他的windows电脑上跑docker-compose 项目。\n我稍微折腾后发现：在2025年的今天，想要在windows上运行docker容器，还是有几个门槛在这里的——很容易因为一些小的设置卡好几个小时。所以，在这里汇总并分享下。\n如果对你有帮助，帮忙点个赞支持一波 ^^\n评估是否有必要 就像上面提到的，是因为条件受限才考虑在windosw上跑docker 应用。\n如果以下情况满足3条以上，那这种情况适合你——\n手头没有类Linux环境（如 Ubuntu、Rocky） 手头有运行Linux的环境，但是硬件不是x86架构的 偶尔跑一些docker项目（即docker 应用不需要 7*24 长时间运行） 只运行docker 应用，不需要开发docker应用 另外在windows 上运行docker 应用对软硬件系统也是有要求的：\nwin10 要求21h2以上版本 + 64位 家庭版 或 专业版 或 企业版 win11 要求22h2以上版本 + 64位 家庭版 或 专业版 或 企业版 需要CPU支持虚拟化技术（Intel VT-x 或 AMD-V），并在BIOS 中启用 4GB 以上内存 需要有管理员权限，以便安装docker-desktop程序和windows组件 微软官方文档在这里\n安装 如果以上的诸多条件和限制，还没有打消你的念头。好吧，头铁的朋友，这篇文章就是为你而写的！\n0. 准备工作 检查上述软硬件要求是否满足\n需要自备梯子，主要用于拉取docker image文件\n1. 安装wsl wsl 全名为 \u0026ldquo;Windows Subsystem for Linux\u0026rdquo;，为windows 下运行docker 的底层环境——毕竟windows 和 Linux 是两套内核。\n安装起来倒是不复杂，一条命令：\n1 wsl --install 重启后，查看wsl版本信息：\n2. 安装docker-desktop 从docker 官方网站 下载Docker-Desktop for windows AMD64。\n双击docker-desktop安装包执行安装，安装完成后“重启”电脑。\n3. wsl 和 docker-desktop 关联成功 重启后，会看到wsl 启用成功的通知：\n因为，是先安装的wsl 后安装的docker-desktop。docker-desktop 此时已经默认调用了wsl 集成环境。（不用手动修改）\n使用命令也可以看到，两者已经关联并运行了：\n同时，电脑中会多出一个Linux 的企鹅图标。不用管它。\n4. 修改docker-desktop 设置 在docker-desktop 的设置中，有三处需要修改。\n因为，docker-desktop 安装时默认使用C盘保存 docker 镜像文件。 下载镜像文件多了后，很容易导致C 盘空间占满。所以这一步，需要手动更改镜像文件保存路径（image location）。\n例如，我改为 *E:\\docker_images* 目录下：\n另外，还需要指定代理环境（使用梯子代理服务端口），以便后续拉取docker 镜像： [Option] 如果没有梯子，可以尝试使用国内的docker 镜像站（注：可能不稳定）：\n拉取镜像\u0026amp;验证 参照以之前n8n 自部署 (nxlan.cn)初始化案例。这里先创建n8n项目目录\u0026quot;n8n-data\u0026ldquo;和 \u0026ldquo;docker-compose.yaml\u0026ldquo;项目文件。\n然后使用以下命令启动该项目——\n1 docker compose up -d 启动成功日志——\n同样的，docker-desktop 中的images 界面可以看到刚刚拉取到的镜像文件和相关信息（版本，大小，运行情况）：\n最后，登录本地n8n环境。证明所有安装完成。\n补充说明 v4.45版本额外操作 [v4.45] 版本的docker-desktop 版本有bug，无法在“设置”中修改images镜像保存位置，所以只能通过powershell 命令行操作。\n注： 经测试[v4.48] 版本修复了这个bug，无需以下命令行方式修改，通过图形化设置界面就可以修改。\n1 临时关闭wsl 服务\n1 wsl --shutdown 2 导出并备份wsl 原有数据文件\n1 2 #先备份到E盘docker_images目录下 wsl --export docker-desktop E:\\docker_images\\DockerDesktopWSL.bak 3 注销原有docker-desktop 数据文件\n1 wsl --unregister docker-desktop 4 导入并重新关联新位置的docker-desktop 数据文件\n1 2 #重新导入备份数据文件至硬盘新位置——E:\\docker_desktop\\DockerDesktopWSL wsl --import docker-desktop E:\\docker_desktop\\DockerDesktopWSL E:\\docker_desktop\\DockerDesktopWSL.bak --version 2 wsl 运行环境限制 目前测试发现，受限于wsl的安全策略：\nwindows环境下的docker实例，访问某个目录中的数据时，不能使用相对路径，必须使用绝对路径。\n例如，希望把 docker-compose.yaml 所在目录下的 app\\data同时挂载给 docker A，不能这么写——\n1 2 volumes: - ./app/data:/docker-app/data 而是需要使用绝对路径——\n1 2 volumes: - /mnt/host/app/data:/docker-app/data 后续如果需要卸载wsl和docker-desktop 卸载docker-desktop 和平常卸载软件没什么区别。\n卸载 wsl 也是一句命令即可：\n1 wsl --uninstall ","date":"2025-10-18T22:40:42+08:00","image":"https://r2.blog.nxlan.cn/PicGodocker_compose_up2.png","permalink":"https://blog.cba.nxlan.cn/p/windows_dockerdesk_install/","title":"2025年了，还需要在Windows下安装docker-desktop环境么？"},{"content":"写在前面 上次写dify上的 AI 面试官案例后，没几天就看到了BOSS 上的一条回复——\n哈哈，是不是有种一眼看穿的感觉——是的，这里HR调用了AI，结合岗位和面试人员背景提出了三个不同维度（ 分别是：产品mvp的规划和定义，推进中的协调和调整，上线后的反思和改进）的问题。\n今天，将在n8n上实现相同AI智能面试官的任务。\n不同于dify 工作流中需要特别关注对话内容递进和信息流转，本文会更为关注Agent框架下实现相同需求的差异和优缺点。\n完成这个任务，后续可以作为一个MCP tool 再被其他Agent调用。例如，面向整个招聘环节的 “HR AI招聘系统”。\n通过本文，可以收获以下内容：\n知识库文档切分、向量化和入库的细节 多轮对话中，使用思维链（CoT）提示词约束LLM行为的同时，也发挥Agent更为优秀的输出能力 RAG召回后，如何与LLM协同完成问题的回答 多轮对话中，Agent与Tools、LLM 之间的调用关系 同一个应用，不同实现路径（Workflow vs Agent ）间的差异 效果展示 老样子，先上效果图。\n本地知识库内容向量化入库：\n通过Agent 完成多轮面试问答：\n面试问题总结和评估：\n实现步骤 1. 上传文档，并使用RAG技术分片存储 还是使用之前预处理过的两个面试问答文档：\n不过，需要把它们上传到n8n docker 应用中去，这里我是放在 /home/node/upload_files/ 文件夹下：\n向量数据库就使用之前dify中安装好的Weaviate。\n因为在Weaviate数据库建立的时候没有设置API Key，这里在n8n中只需关联Weaviate链接地址（http://difiy.host:8080）就可以查看内部数据表了：\n如果本地没有向量数据库，也不希望把文档吐给云上的向量数据库，可以使用n8n提供的本地“简单向量存储”工具。该工具是把向量和分片数据储存到内存当中的，就是重启后那些处理过的数据会全部。只建议在测试环境中使用这个“简单向量存储”。\n最终，将以上节点组合起来，就是第一个工作流——知识库文档向量化并存储。\n手动点击”执行“，看一眼输出结果。\n在n8n的logs 中可以清楚地看到向量化的过程：\n读取本地目录中的文件 \u0026ndash;\u0026gt; 进入循环 \u0026ndash;\u0026gt; Weaviate 向量库工具，先是根据文档中的切分标记“BBBBBB” 对文档分片 \u0026ndash;\u0026gt; 然后使用embedding 模型计算每个分片的向量值 （这里设定每次处理20个分片） \u0026ndash;\u0026gt; 第一个文档切分为了101片，所以经过6次的重复计算 \u0026ndash;\u0026gt; 将每个分片的信息（含原始文本内容和元数据）与embedding 计算后的向量值关联在一起储存 \u0026ndash;\u0026gt; 这样就完成了第一个文档的切分和向量化存储 \u0026ndash;\u0026gt; 同样地过程处理第二个文档 \u0026ndash;\u0026gt; 直至所有文档处理完成，循环结束\n例如，其中一个分片按照以下格式存储在向量数据库中——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ID: 0001f149-5a73-4666-975c-d9e1ae6bab59 values: [0.00667897193, -0.00112631731, 0.402422339, -0.0144329099, 0.0131256515, 。。。。-0.00670505082, 0.00998431537, 0.00638512149, 0.00762740057, -0.00297592231] metadata: blobType: \u0026#34;application/vnd.openxmlformats-officedocument.wordprocessingml.document\u0026#34; loc.lines.from: 349 loc.lines.to: 357 source: \u0026#34;blob\u0026#34; text: \u0026#34;4. 你老家哪里的？\\n\\n技巧：这个问题主要想了解你的稳定性，会不会做一段时间就辞职。\\n\\n模板：\\n\\n我的老家是\\t ，是一个\\t 的地方。但是因为\\t，我更想要在这个城市长期发展，而且我的家人都很支持我的选择。\\n\\n回答示例：我老家是西双版纳的，是一个旅游城市，以后有机会去的话，可以给你推荐一些很值得打卡的地方。但是比起在老家发展，我更想留在这个城市，因为对我所读的专业来说，这里能给我提供更多的就业机会和发展，所以找工作选的这里的公司。而且家里人都很支持我的选择，我打算长期留在这边发展。\u0026#34; 其中，有原始的文本，有文档切片元数据（指明了对应原始文档的第349行到357行），还有一长串向量。\n而这个向量，就是后续向量库检索时的条件。\n另外，有两点再补充一下：\n不同于Transformers 模型，encoder时是按照每个Token 完成向量化计算（得到512x1 的矩阵）。\nRAG中embedding 向量化的对象是全部的输入。\n例如，下面查询句子\u0026quot;hello Qwen3\u0026quot;的向量结果——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 向OLLAMA API接口发起这段话”hello Qwen3“的向量查询，使用的模型是qwen3-embedding:8b 。 curl http://ollama.host:11434/api/embed -d \u0026#39;{\u0026#34;model\u0026#34;:\u0026#34;qwen3-embedding:8b\u0026#34;,\u0026#34;input\u0026#34;: \u0026#34;hello Qwen3\u0026#34;}\u0026#39; # 返回的内容格式如下，其中 embeddings对应的就是一个 4096 长度的列表。 { \u0026#34;model\u0026#34;:\u0026#34;qwen3-embedding:8b\u0026#34;, \u0026#34;embeddings\u0026#34;: [[0.005400424,0.004791923,-0.009427172,-0.01674511,-0.0012024766, ...... 省略中间部分 -0.00039164326,-0.015191954,-0.011491347,0.00041572566] ], \u0026#34;total_duration\u0026#34;:7977521700, \u0026#34;load_duration\u0026#34;:100531700, \u0026#34;prompt_eval_count\u0026#34;:4 } 所以，拆分后的100个问答对，就会产生100个向量。而向量长度取决于模型。\nqwen3-embedding:8b 模型，把内容向量化后，默认是得到一个4096x1 的一维向量。上图最右侧展示了 4081 ~ 4095 这几个位置的向量值。\n不同的embedding 模型其向量化后的长度不一定一样。\n使用的时候需要注意筛选：越长的向量化输出，带来更精细的的向量数据和更大的资源开销。\n这边根据网上资料，整理了常见的向量化模型——\n模型 (Model) 开发者/机构 (Developer/Organization) 最大/默认向量长度 (Max/Default Dimension) 备注 (Notes) 支持可变维度的先进模型 Qwen3-Embedding-8B 阿里巴巴 (Alibaba) 4096 支持MRL，可截断为 32 到 4096 之间的任意维度。上下文长度 32k。 text-embedding-3-large OpenAI 3072 支持MRL，API中可指定 dimensions 参数，例如 256, 512, 1024, 1536。 text-embedding-3-small OpenAI 1536 支持MRL，API中可指定 dimensions 参数，例如 256, 512。 主流闭源模型 text-embedding-ada-002 OpenAI 1536 不支持可变维度。曾是业界最广泛使用的API模型之一。 text-embedding-gecko Google (Vertex AI) 768 Google PaLM 2 系列的 embedding 模型。 主流开源模型 bge-large-en-v1.5 BAAI (智源研究院) 1024 MTEB 排行榜上的常客，性能强大。 e5-large-v2 Microsoft 1024 另一个经典的、性能优异的开源模型。 2. Agent 实现多轮问答和记忆 Agent 这部分内容，我们之前在AI 新闻采集与推送助手的案例中有经验：\n主要由 LLM + memory + tools 几部分组成。\n如今在同样的多轮对话场景下，Agent又是如何实现这些过程呢？\n与workflow 方式不同的是：Agent中分析、判断、推理能力成为出厂默认配置，不再需要人工提取和干预，而是交给AI 自主决策——俗称AI max 。\n具体操作层面：\n通过带有思维链的提示词，引导AI完成上述对话环节分析和打分评估。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 角色 你是一个有十年面试经验的专业HR。现在是我的专属\u0026#34;面试顾问\u0026#34;，你的任务是模拟面试官进行面试提问和打分，然后输出专属改进建议。 最终你需要将所有分析和建议，整合为一个可用于展示的Markdown格式文本块。保持专业、乐观、敏锐！ 有一个核心工具供你调遣： quary vector info: 检索面试问答知识库中，与提问问题相关的回答案例。 你的行动指令： 第一论对话：发起初始对话 模型HR面试官语气，询问面试人员年龄 工作年限 过往项目经验 面试岗位等相关信息。 if (用户拒绝回答个人基本信息的内容) {以HR的语气（略显严厉），引导用户回答基本信息} elif (用户回答了与询问的问题无关的内容) {忽略与询问问题无关的回答，只关注用户针对问题已做出回答的内容} elif (用户回答过程中遗漏了某个问题) {以HR的语气（友善 积极），引导用户继续回答剩下遗漏掉的问题} else (用户针对面试官提出的问题 都做出了回答) {将用户回答存入记忆中，并开启第二轮对话} 第二轮对话：完成问题提问和用户回答内容采集 根据上一轮用户提供的面试信息，从专业HR 角度提出三个面试问题{{ ques_list: [问题1,问题2,问题3]}，并引导用户做出回答。 用户可以一次一个回答相关问题，也可以一次性回答这三个问题。 if (用户没有回答任何一个问题) {以HR的语气（略显严厉），引导用户回答相关问题} elif (用户回答了与面试问题无关的内容) {忽略与面试问题无关的回答，只关注用户针对问题已做出回答的内容} elif (用户回答过程中遗漏了某个问题) {以HR的语气（友善 积极），引导用户继续回答其余问题} else (用户针对面试官提出的问题 都做出了回答) {将用户回答存入回答列表 {{ userans_list: [问题1回答, 问题2回答, 问题3回答]}}，并开启第三轮对话} ## 示例 ### 示例 1 - 面试者背景：毕业于计算机科学专业，并在学校期间参与了两个项目。 - 申请岗位：初级软件开发工程师。 - 生成的问题： - 在学校项目中，你所遇到的最大的技术挑战是什么？你是如何解决的？ - 在团队项目中，当团队成员之间的意见存在分歧时，你是如何处理和协调的？ - 你最近学习了哪些新的技术？你是如何将其应用到实际项目中的？ ### 示例 2 - 面试者背景：有 5 年的市场营销经验，并且领导过 3 个成功的营销活动。 - 申请岗位：市场经理。 - 生成的问题： - 你能分享一下你所领导的最成功的营销活动的案例以及关键策略吗？ - 你是如何衡量营销活动的 ROI 的？请举一个具体的例子。 - 当面临预算削减时，你会如何调整你的营销计划？ ### 示例 3 - 面试者背景：原为教师，现准备从教师转行到人力资源岗位，虽无正式的 HR 经验，但曾组织过学校的招聘活动。 - 申请岗位：HR 专员。 - 生成的问题： - 你为什么决定从教师转行到人力资源？ - 在组织学校招聘活动中，你的最大收获是什么？ - 你是如何处理员工之间的冲突的？能否举一个你在教师工作中的具体例子？ 第三轮对话：根据用户回答的内容,对用户回答的每个问题的进行综合评分。 - 第一步：提取问题知识库中标准回答建议。 使用工具\u0026#34;quary vector info\u0026#34;查询面试知识库中{{ ques_list[id] }}的标准答复，并根据该答复内容整理为标准回答思路和要点 {{ standans_list[id] }}。 - 第二步：用户回答评分 先比较用户回答{{ userans_list[id] }}和知识库内容{{ standans_list[id] }}中的回答思路和要点。 然后从以下几个方面综合评估用户的回答： 1. 用户回答的逻辑性、完整性和相关性 - 考察表达能力、专业知识和应变能力 - 注意候选人的非语言表现(如语气、节奏等) 2. STAR原则应用 - Situation: 是否清晰描述背景情境 - Task: 是否明确说明任务目标 - Action: 具体行动是否专业有效 - Result: 结果是否可量化且有说服力 3. 用户回答的思路与知识库中回答内容的差距 - 评估用户回答思路与知识库回答思路是否一致 - 对其中相同部分予以肯定，不同部分后续步骤中会对此提出改进建议 - 第三步：依据上一步骤中三个方面的情况，完成对用户回答的打分。 - 采用5分制评分(1-5分) - 1分: 完全不符合要求 - 3分: 基本达到要求 - 5分: 表现卓越超出预期 - 第四步：以markdown 格式输出面试评分结果和相关建议。 - 需提供具体改进建议 - 保持专业性且建设性 - 突出候选人的优势与不足 请严格按照以下指导来组织信息，但不要在你的最终输出中包含模板本身的 ```markdown 包裹标记或任何非Markdown的解释性文字。 Markdown内容结构指导（请填充实际内容）： ### 🚀 面试 **专属建议** **🌟 面试问题 评分：** * --- * **面试问题：** {{ ques_list[id] }} * **回答思路：** {{ 第二步中根据{{ standans_list[id] }}整理的回答思路和要点 }} * **分数：** {{ 第三步中此问题评分 }} * --- **💡 优化建议：** [基于该面试问题，给出一个综合性的建议。 例如： - 如果回答思路与知识库中相符，重点检查回答细节：可以建议加强问题场景的故事性，或者建议增加相关定量描述。例如：“建议增加 问题场景的细节描述，这样面试官在了解问题后，会代入自己公司的产品进行替代问答。有利于引导面试官在自己描述的场景内提问，并满足他需要的信息和能力证明”。 - 如果回答思路与知识库不相符：可以建议先明确面试官是想问什么，他问这个问题是需要考量面试着的哪些素质。作为面试者如何通过1-2个案例体现出这些能力。例如：“建议按照[回答思路] 答复面试官的提问，具体答复案例可以是这样：[按照回答思路举一个案例]”。 ] 通用要求： 你的建议要具体、有建设性、信息充分，并体现出是对面试人员的综合考量。 语气要积极、专业，充满洞察力。 是不是有点意外：之前挺复杂的工作流，在Agent 这里一个节点就搞定了。\n效果就是开头效果展示里面的样子 。\n这里稍微提一句 “记忆”模块，它的作用就是在Agent工作前输入历史对话信息，Agent输出后追加本次对话内容（Human 和 AI对话内容）。所以，随着对话内容变多“记忆”的内容也是越来越多的。 也正是因为有“记忆”这个模块，每一次对话时 Agent 才知道当前对话主题是什么，进入到什么阶段了。\n下图可以看到**“记忆”模块在Agent中的位置**——一开始和最后。\n3. Agent 调用知识库内容完成任务的详细步骤 本章节着重看观察 Agent是如何协调大模型和RAG 知识库完成面试问题回答和评价的：\n有了这些细节，再回过头看之前的 RAG 检索示意图，是不是清楚多了？\n其中2.1 ~ 2.3 对应 RAG的检索，3.1 ~ 3.2 对应RAG的提示增强， 4.1 对应生成回答 。\n4. Agent 、LLM与Tools 间的关系 就像提示词中写得：Agent 调用 tools 工具（\u0026ldquo;quary vector info\u0026rdquo;）只在会话进行到第三轮时，才会去RAG检索。并不是每次都会调用的。\n后续，如果有其他工具，例如 \u0026ldquo;背调工具\u0026quot;就也可以加进去。这样扩充能力后，Agent可以自主判断、执行的操作就越来越多。 今年除了 claudecode cursor Trace这些常见的编程工具外，Manus 应用也火爆起来，背后就是大家都希望AI 可以做得更多、更好，而人类只需要检查AI 的输出就好的美好愿景。\n可以说 Agent 的魅力也在 这种“可以丰富扩展”的想象。\n具体落实到产品层面的话，还有不少的难题需要处理，后续会陆陆续续展开一些。\n对比这两种路径 同样一个需求 是使用Workflow 还是 Agent 背后关乎成本、需求、实现平台等等因素。\n角度 Workflow Agent 不同点 处理方式\u0026amp;难度 围绕SoP，设计每一步的输入、输出内容和格式 使用提示词控制LLM关注方向，实现复杂场景下的内容输出 LLM 模型能力要求 一般 高 输出内容 可以稳定执行，但是语言上不够自然 更接近于自然沟通、交流 范围外输出 不会发生 会发生，可能存在一定泄漏数据或提示词的风险 Token 消耗 低 高 相同点 知识内容更新 更新知识库就行，无需调整工作流 同样，只更新知识库就行，无需修改Agent 提示词 知识库使用方式 作为上下文的一部分 同样，也是作为上下文的一部分 再补充两个细节：\n在本案例中，我一开始打算全部使用本地显卡上运行的 deepseek-r1:8b或 qwen3:8b 两个模型实现。\n结果发现：在RAG检索和提示增强时，用本地的8b模型还可以，但是Agent 中也使用这两个8b 模型都无法正确理解每轮对话的关注点，导致对话无法正常执行下去。\n而与之对比的是 dify 中，全部使用的是本地 qewn3:8b 模型。\n在token 消耗这块，Agent 也是比workflow 多的。主要多在两个方面：\nAgent 每轮对话的内容都保留在 一个记忆对象中。随着对话内容多了，这里面的内容是越来越长的。而且容易有无关的内容也混在其中，无关内容多了后也会干扰LLM模型的注意力。 而workflow 中有分流节点（类似 if/else）的存在，即需要在某个执行节点查看只与本节点相关的记忆即可。\n因为Agent对模型能力要求更高了，所需的算力也更多，自然花费也大一些。 在下图中也体现了这一点：\n小结 Workflow 的路线像是自己做菜——洗、切、炒之外，菜板、刀、调味料这些基础条件也需要自己操行。\nAgent 的路线更像是 中央厨房做好了预制菜——买回来，自己只需要加点喜欢的菜、上锅热一热就齐活了！\nRAG 本质上是一种工程解决方案：目的是在有限的上下文窗口内，提供预先处理过得高质量数据。\n从而提高LLM的注意力和回复质量，同时规避“幻觉”和频繁调用外部工具的麻烦。\n感谢你看到这里，听我分享这些。我们下期见。\n","date":"2025-10-14T11:29:56+08:00","image":"https://r2.blog.nxlan.cn/PicGon8n_chatting_Agent.png","permalink":"https://blog.cba.nxlan.cn/p/n8n_ai_interview/","title":"AI面试官[续] —— 改用Agent的方式实现"},{"content":"AI 多轮对话引子 之前分享过几个AI应用案例：个人兴趣助手、塔罗牌占卜、AI新闻推送助手。\n粗看起来它们的形式和使用工具（插件）各有不同，但是其核心都是工作流——在指定的步骤中完成复杂度不高的任务。\n这里的复杂度不高的原因来自三个方面：\n数据有固定的格式 AI处理问题的逻辑是一条直线，没有“岔路” 处理过程中没有外部的干扰 那缺点就是，只能完成特定场景下的特定问题，AI 含量不高！可能有小伙伴会问了，AI 含量高有什么好处呢？\n哈哈，这也是我之前朦胧的地方。\n所以，今天就通过一个AI 面试官的应用，探讨在复杂度高一些的场景下，AI 多轮对话应用的实现。\n案例：AI 面试官 它的复杂度高在哪里？\n对话内容没有标准格式，并且面试评判标准也不统一。\n面试天然就是一个多轮对话的过程，随着对话深入，对话主题会改变。例如：面试官提问过程就是在从不同维度对候选人综合能力评分的过程。\n真实面试中 可以划分成前后两个阶段。\n前期：面试JD 输入\u0026ndash;\u0026gt; 候选人匹配 \u0026ndash;\u0026gt; 候选人初筛 \u0026ndash;\u0026gt; 约定面试时间。\n面试环节： 候选人介绍自己 \u0026ndash;\u0026gt; 面试官根据岗位和候选人背景提出相关面试问题 \u0026ndash;\u0026gt; 候选人回答 \u0026ndash;\u0026gt; 面试官继续从不同维度对候选人提问 \u0026ndash;\u0026gt; 最终形成候选人综合评分（包括：岗位硬技能、软技能、项目经验、发展和稳定度等）\n这里，跳过前期沟通阶段，直接进入面试环节开始后的对话和流程。\n梳理后，发现完整的一次面试问答至少需要 4轮对话——\n如何实现？ 参考梳理出的完整对话过程 。如何在一个工作流（workflow）中逐步识别会话重心发生了改变就成为了关键点。\n相比于Agent 的方式，workflow中的识别不是依赖LLM 进行语义分析和逻辑判断，而是依赖工作流中多个变量实现——如问题变量、用户回答变量状态的变化。\n这里，直接上结论——\n对话 需要判断的节点 具体实现 第一轮 对话内容是否和面试相关 对话内容和面试相关：就继续后续步骤。 如果内容和面试不相关就直接答复用户“内容和面试无关，无法答复。” 第二轮 候选人信息和面试岗位信息是否充分 信息充分：就产生面试问题清单。如果信息不充分：就不生成这个清单。 第三轮 面试过程中，候选人 可能只答复其中一条问题，也可能跳着回答 用户回答所有问题：就产生用户回答清单。如果用户没有回答全部问题：就引导用户回答，并不生成回答清单。 第四轮 后续对话，是否基于问答内容生成新的面试问题。 问答内容会放入“记忆”中。当用户回答完所有问题后，会重新进入问题生成环节，该环节会参考之前的记忆。 干扰测试 如果我在 面试过程中 胡言乱语 会发生什么？ 明显的胡言乱语，会被第一轮识别过滤掉。如果是在面试过程中文不对题，则会在第三轮用户回答过程中识别。 效果图 完整的工作流\n对话日志\n用户针对提出的问题，乱序完成回答后，面试助手会基于问题和用户回答 打分并提出改进建议——\n新一轮对话提出的问题，就参考了之前用户回答中提到项目内容——\n例如：这里就提问了在“AI 知识库”这个项目中，如何平衡数据质量和数据覆盖率的问题。\n动手实现 这里，会着重展示如何使用工作流中的多个变量控制会话进入到什么阶段。\n第一轮 对话 需要筛选与面试相关的内容。如果不相关就回答：“与面试话题无相关。抱歉，我不能答复。”\n实现比较简单，直接使用dify的“问题分类器”——\n1 2 3 4 if (用户问题 与面试话题有关): 就进入下一轮话题——生成符合应聘者背景和岗位需求的面试问题 else : 与面试话题无关的内容，直接回答“不能答复” 第二轮 对话 需要引导用户输入面试所需的人员信息和岗位信息。如果用户一次说不完 就提醒用户补充这些背景信息。\n这里的判断逻辑为——\n1 2 3 4 5 6 7 8 9 if (全局变量\u0026#34;question\u0026#34; 为空): if (模拟面试中所需的面试背景信息充足): 生成面试问题清单 存储至变量 question中 同时，答复用户面试问题 else: 引导用户补充更多的面试背景信息 else: 已经生成面试问题了，可以进入后续步骤——面试问题回答环节\t第三轮 对话 需要检查用户输入的回答对应哪个问题，然后提示用户继续回答全部问题。\n提示词——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 角色 你是一位专业且高效的用户回答与问题匹配智能助手，负责将用户的回答精准匹配到之前给定的问题，并以指定数组形式输出。同时，能从专业HR的视角引导用户完成所有问题回答。 ## 背景 当前对话历史：{{ chat_history }} 用户最新答复：{{#sys.query#}} 问题清单：{{#context#}} ## 技能 ### 技能 1: 匹配并输出用户回答 1. 仔细查看提供的问题数组，明确问题数量与顺序。 2. 认真阅读用户给出的回答，准确判断该回答对应的问题。 3. 依据用户回答和对话历史，创建一个新数组。在新数组中，按照用户当前及历史回答填充对应问题位置的回答内容，未回答问题的位置用null填充。 4. 最后，将这个新数组以{\u0026#34;userans\u0026#34;: [新数组]}的格式输出。 ### 技能 2: 检查并提示用户回答所有问题 1. 根据技能1整理好的用户回答数组 “userans”，判断用户未回答的问题。 2. 以专业HR的口吻，礼貌且清晰地提醒用户遗漏的问题，并引导用户回答问题清单上的所有问题。 ## 输出示例 在用户初次使用或不明确任务时，按以下示例格式展示匹配输出： - 示例 1：若问题数组是：[\u0026#34;问题 1\u0026#34;, \u0026#34;问题 2\u0026#34;, \u0026#34;问题 3\u0026#34;]，用户的回答是：\u0026#34;这是问题 2 的回答\u0026#34;，那么输出应该是：{\u0026#34;userans\u0026#34;: [\u0026#34;null\u0026#34;,\u0026#34;这是问题 2 的回答\u0026#34;,\u0026#34;null\u0026#34;]}， \u0026#34;more_userans\u0026#34;: “请继续回答 问题1 和 问题3”。 - 示例 2：若问题数组是：[\u0026#34;问题 A\u0026#34;, \u0026#34;问题 B\u0026#34;, \u0026#34;问题 C\u0026#34;, \u0026#34;问题 D\u0026#34;]，用户的回答是：\u0026#34;这是问题 C 的回答\u0026#34;，那么输出应该是：{\u0026#34;userans\u0026#34;: [\u0026#34;null\u0026#34;, \u0026#34;null\u0026#34;, \u0026#34;这是问题 C 的回答\u0026#34;, \u0026#34;null\u0026#34;]}， \u0026#34;more_userans\u0026#34;: “请继续回答 问题A、 问题B 和 问题D”。 - 示例 3：若问题数组是：[\u0026#34;问题 1\u0026#34;, \u0026#34;问题 2\u0026#34;]，用户的回答是：\u0026#34;这是问题 1 的回答。这是问题 2 的回答\u0026#34;，那么输出应该是：{\u0026#34;userans\u0026#34;: [\u0026#34;这是问题 1 的回答\u0026#34;, \u0026#34;这是问题 2 的回答\u0026#34;]}， \u0026#34;more_userans\u0026#34;: \u0026#34;\u0026#34;。 ## 限制: - 所有输入和输出均不包含 XML 标签。 - 生成用户回答数组时，内容必须源自用户输入信息，不得编造用户未提及的内容。 - 输出内容必须在structured_output对象中以json格式呈现，严格遵循此框架要求。 ## 通用要求： - 确保用户回答和提问清单准确源自对话过程。 - 语气积极、专业，展现出洞察力。 当用户回答完所有提问问题后，会存入全局变量“userans” 数组中。\n这样就完成了一问一答两个数组(变量)的映射：\n1 2 3 question = [ \u0026#34;问题1\u0026#34;, \u0026#34;问题2\u0026#34;, \u0026#34;问题3\u0026#34; ] userans = [ \u0026#34;这是问题 1 的回答\u0026#34;, \u0026#34;这是问题 2 的回答\u0026#34;, \u0026#34;这是问题 3 的回答\u0026#34; ] 有了两个数组（变量），后续就可以在循环中，提取每个问题和回答。\n简化下判断逻辑，大概长这样——\n1 2 3 4 5 6 for id, ques in enumerate(question): stand_ans = search \u0026#34;ques\u0026#34; in \u0026#34;HR 问答知识库\u0026#34; user_ans = userans[id] user_score = LLM (judge user_ans reference stadn_ans) Answer.append(user_score) 所以，可以直接使用 question 数组中的问题作为控制循环次数和查询知识库的关键字。\n第四轮 对话 这里有个小问题，如何判断用户是在继续回答问题，还是在发起新一轮问题？\n首先，在第三轮对一问一答两个数组（变量）处理过程中，处理一个问题， question 中就减少一个。全部处理完，question 数组就为空。换个说法就是：如果question 为空，就表示当前是问题产生，而不是问题回答阶段 。\n其次，这里用户每次回答的内容userans并没有被清空，而是随着对话变多 而逐步丰富。\n所以，只要在第二轮的大模型应用LLM2 中，要求生成问题时，同时参考用户当前输入信息（sys.query）和历史回答信息（userans）就可以产出一轮更深入的问答。\n干扰 对话模拟 对话过程中，突然暴躁——说出一句“滚”。 问题分类器会做出回应，答复用户 “与面试话题不相关。抱歉， 不能答复。”\n用户回答 开始答非所问。\nLLM4——就是第三轮中的问题识别与关联助手，会识别并关联 用户回答。以及还有哪个问题是没答复的？\n下面稍微展开下 使用“RAG” 技术实现面试问答知识库的话题：\n为什么需要知识库 使用知识库的原因很简单：LLM 模型在泛化 通用能力上已经可以很好的处理分析问题。例如，写个年终总结报告、生成一段贪吃蛇代码。\n但是，如果具体到垂直领域的问题，表现就比较差了。因为它没有这部分领域的准确数据！没有怎么办？大模型就容易出现 “一本正经胡说八道”的情况。此时，问题不在模型不理解、判断不了，而是由于该领域数据不足，无法准确回答。\n“知识库”实现了内部私有数据的检索和展示。包含了，数据入库、数据检索和增强LLM上下文 三部分。\n先看数据入库阶段——\n然后是检索和返回LLM处理过程——\n检索模式不止这一种。更多的方式，可以参考：\nAdvanced RAG Techniques: an Illustrated Overview | by IVAN ILIN | Towards AI\n在 dify 中实现面试知识库 大致分为 数据清洗 \u0026ndash;\u0026gt; 数据切片 \u0026ndash;\u0026gt; 数据向量化\u0026amp;入库 \u0026ndash;\u0026gt; 召回验证几个阶段。\n1. 拿出准备好的数据集 ​ \u0026lt;面试100 问\u0026gt;\n2. 数据集 内容预处理（清洗） 经过测试发现，dify 对pdf 文档的处理时无法识别分段标识符（我这里用的 “BBBBBB” 作为分段标识符）。\n所以需要先把pdf 转为 word 。然后，使用正则添加分隔符——\n同样可以在关键位置添加换行符——\n3. 上传文档。数据分段，并使用embedding 工具完成向量化 这里100个问题被划分成了106个数据块（因为有两个问答对中 重复使用了1. 2. 3. 数字开头，被分段标识符 “BBBBBB” 多划分出了几个段落）\n4. 指定 索引和混合检索方式 5. 召回验证 ​ AI 面试官案例中，主要是希望让大模型参考专业HR的标准回答思路，而不是具体内容。\n​ 所以 重点参考回答思路中内容。\n最后，RAG在哪些场景中效果不好 RAG 技术中很关键的步骤就是 根据输入的内容 快速检索出 与之接近的（向量）数据块。\n为了更好的检索效果，引入了混合检索和重排技术。在检索时，同时根据问题向量(vector index)和语义（summary index）召回数据片段。这里权重比为 7:3。\n例如使用之前的面试问题提问，查看召回内容：\n但是，如果有以下三种情况还是容易出现数据检索不出来的情况：\n相同的关键字 散落在文档里很多地方。\n例如，小说中的 主人公“许仙”。提问：“许仙和白素贞在断桥有几次相遇？” RAG即使检索出来，也无法保证召回的片段覆盖了所有相遇的剧情。\n检索语义不是通用的，而是带有垂直领域的内容。\n例如，”那个在\u0026lt;还珠格格\u0026gt;中饰演尔康的演员还演过其他的什么影视剧？”\n这种知识就很难检索出来——“饰演尔康的演员”本身又是另一个检索的结果。知识库得先检索出这个演员的真实名字 ，还要存有每部电视剧的 演员-角色 关系表，然后才可以通过演员信息 检索出所有出演的影视剧。\n还有一种场景，也比较常见：知识库本身质量不高。\n例如：文档格式不统一，文档覆盖面不足，导致检索的时候 得到的内容离问题相关度很远。\n此时，属于RAG 技术也帮不了的情况。\n前两种是 RAG 机制的导致的，无法避免。最后，这个属于领域知识数据 没达到可用的程度。\n除了RAG 还经常听说模型微调？ 下面是它们两者间的相同和不同的地方——\n相同点 不同点 补充模型在某个领域特定知识，解决幻觉问题 实现路径不一样 RAG 类比：开卷考试——考试时现查 根据问题去知识库检索相似内容或案例 微调 类比：考前复习——把之前考试真题全部做一遍 通过多轮训练，强化模型在该领域的能力（不仅仅是数据上的，还有推理和逻辑上的针对性强化） 彩蛋 同样的案例，后续会使用n8n 的Agent模式再实现一遍。\n到时会豁然开朗：在多轮对话场景中，workflow 和 Agent 有什么差异；以及深入到RAG 调用环节的细节。\n从产品角度看，AI 应用效果的量化很重要。即 AI 应用内部每个节点状态的可观测性是非常必要的。\n以下是通过可观测工具 Langfuse 快速筛选出 “过去三天中 所有执行失败”的那些对话——\n是不是非常方便？有了这些对话，为下一步AI 产品迭代升级 提供了基础的数据支撑。\n举个例子：用户反应某次对话执行时报错了。那到底是提示词问题，还是大模型响应超时，还是发生了预期外的问题，是不是得有个统计数据在这里呢？\n","date":"2025-10-01T16:46:22+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-s3ohfmel.png","permalink":"https://blog.cba.nxlan.cn/p/dify_ai_interview/","title":"AI面试官 —— 打造你的专属加薪助手"},{"content":"写在前面 「案例来源」 B 站 \u0026lt;秋芝 2046\u0026gt; 从0开始“做”一个Agent！。\n完整的步骤\u0026amp;内容大家可以看原作者的视频和文档，这里就不再复述一遍了。\n本文主要是想分享三块内容：\n修复作者提供的AI新闻采集工作流的bug——没有“创建时间”字段的值。如果缺少这个值，就无法实现按照日期筛选最近两天AI 新闻的任务。 说起来这个案例逻辑也不复杂，但是工作流与飞书应用的API接口对接起来特别繁琐（不同应用接口的数据格式都不一样，需要人工一项一项对接上 才行） 在读取、整理新闻列表并发消息给飞书机器人这个工作流中， Agent 的提示词比较有意思，为后续进一步深入学习提供了不错的案例。 通过本文你可以获得以下收益：\n熟悉n8n 平台操作\n如：变量计算和引用这块——n8n 这部分做得非常好了\n对接飞书应用API接口时，需要注意的地方\n如：涉及多维表格 增删查；日历会议列表筛选；feishu 机器人webhook对接；还有应用权限开放。\n浅聊 Agent 提示词框架和思维链（CoT）\n效果展示 每天早上8点，飞书机器人会收到推送的 AI 新闻摘要和最近的会议安排，并提醒用户优先阅读其中重要的文章。\n这个案例分为两部分（阶段）：\nAI 新闻整理存档工作流：通过RSS 订阅，定时搜集AI 新闻频道（如这里的 新智元 腾讯科技 量子位）中的新闻，然后把新闻的标题 摘要 链接等数据按照统一的格式存入飞书多维表格中。\n这个工作流，经过我的完善，已增强为写入新闻数据的同时，删除7天前旧的新闻。\n分享链接放在公众号【AI 热气球】中，感兴趣的小伙伴可以回复 917 获取。\n新闻推送工作流：每天早上8点， 另一个工作流（下图）会读取多维表格中今天和昨天的AI新闻和我最近7天的日程信息，生成当天AI新闻摘要与日程提醒。\n要点1 1. n8n平台上，工作流中的变量赋值和引用非常友好，达到所见即所得的效果。 以“AI新闻整理存档工作流”为例——\n“INPUT” 为上个步骤的输出内容：整理好的新闻标题、日期、内容、媒体、链接信息。\n在“Edit Fields”这个工具的帮助下，可以直接通过拖拽的方式完成新变量的赋值。\n这样一来大大简化很多过程数据的处理步骤。如果对比coze dify平台中的处理方式，肯定会认可n8n 的这种处理方式：\n在coze 和dify 中都需要使用代码工具，在代码中重新定义一个新的函数，然后把上一步生成的新闻对象 Array[object] 在for循环中一个一个提取出来，并在每个循环处理中 追加新的变量名和变量值，最后生产一个新的 Array[object] 类型数据 。\n2. 简单数据的处理在过程中就可以快速完成 这里举两个例子，一个还是在“Edit Fields”这个工具中。\n注意下图中 我新增了两个字段（变量）——“创建时间”和“7天前的时间”。前者用于写入飞书多维表格中的“创建时间”字段，方便后续筛选。后者则用于删除多维表格中7天前数据的筛选条件。\n这里时间的计算，直接调用了javascript 的Data 函数一步完成。并且n8n 会把每个变量的值直接显示在对应变量的下方——那行紫色的文字！\n另一个例子：在写入新闻链接数据到多维表格时，可以直接调用正则表达式——对“新闻链接”数据中可能存在的换行符(\\n) 完成替换。\n3. 如果遇到复杂的数据处理，怎么办呢？ 当然n8n 也有code 功能模块，默认启用的就是javascript。这一点不像 dify 那样需要一个额外的code容器来完成python或javascript 代码的执行。\n这里的例子是：上一步已经从飞书多维表格中筛选了7天前的新闻，但是在删除多维表格数据时，用到的是“record_id”字段。所以，这里使用code 工具只把多维表格中数据所在行的“record_id” 提取出来，交给下一步。\n要点2 在之前“coze塔罗牌工作流华丽转身”的文章中，提到了多维表格API 接口的使用。在那个案例中，主要是用到多维表格的“读取”和“写入”两个功能。\n在本案例中，进一步升级—— 有多维表格的查询、写入、删除，还有飞书日历的查询。\n所有飞书工具的调用，离不开n8n社区中一个非常好用的工具——Feishu Node。\n首先它是社区插件不是飞书官方提供的，其次它比coze 提供的接口的还好用！\n这一个插件汇总了飞书平台支持的107种操作，下图只展示了其中“多维表格”的一部分操作——\n这些操作都是通过飞书 官方API 的接口调用实现的，所以肯定要满足飞书平台API接口的格式要求和限制。\n以多维表格中日期字段的填写要求为例——\n所以，这也是文章开头提到“每个接口都需要人工一个一个对接”的原因。\n具体怎么对接，大家还是自己跑一遍试试就知道了。\n本案例的工作流我已经放在公众号【AI 热气球】中，感兴趣的小伙伴可以回复 917 获取。\n这里，我提醒两个踩过坑的地方：\n1: 飞书开放平台中 相关应用的权限开放 这里的相关权限类型是“应用身份”，不是“用户身份”。\n如果权限不匹配，会导致相关API 操作失败，但其实不是n8n 工作流中配置的问题——\n2: 用到的表格需要开通 应用的“可编辑”权限 要点3 个人以为：这个案例中，最有含金量的是在第二个工作流中调用Agent 思维链(CoT)的方法——体现了人类抽象思维的具象表达。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 你是我的专属AI助理“新闻报通”！你的使命是帮我洞察最新的AI动态，并结合我的工作日程，智能推荐感兴趣的内容和安排行程。在没有行业大事发生时，你也会关心我的生活，推荐放松娱乐活动。 最终你需要将所有分析和建议，整合为一个适合在飞书卡片中展示的Markdown格式文本块。保持乐观、敏锐、有创造力！ 我有两个核心工具供你调遣： news：用它来抓取过去2天内飞书多维表格里最新、最有料的AI新闻。此工具会返回一个新闻列表，列表中的每条新闻都包含：新闻标题、发布日期、发布媒体、核心内容/摘要、以及原文链接。 daily：用它来查看我未来7天的飞书日程安排。此工具会返回日程事件的日期、时间、事件标题。 你的行动指令： 第一步：信息收集 立即使用【最新新闻查询】工具，获取最新的AI新闻列表（每条新闻包含标题、日期、发布媒体、摘要、链接）。由于消息长度限制，精选其中12条新闻。 同时，使用【日历查询】工具，获取我未来7天的详细日程安排。 第二步：智能分析与建议（输出为纯Markdown格式） 你的核心任务是生成一段单一、完整的Markdown文本。此文本本身就是最终要在飞书卡片中呈现的内容。请严格按照以下指导来组织信息，但不要在你的最终输出中包含模板本身的 ```markdown 包裹标记或任何非Markdown的解释性文字。 Markdown内容结构指导（请填充实际内容）： ### 🚀 AI圈今日速递与【**专属建议**】 **🌟 今日AI新闻看板：** {{#if (tool_output.latest_news an_array_with_items)}} {{#each tool_output.latest_news as |news_item|}} * --- * **标题：** {{news_item.title}} * **发布日期：** {{news_item.date}} * **发布媒体：** {{news_item.source_or_media}} * **核心摘要：** {{news_item.summary}} * **原文链接：** [点击查看详情]({{news_item.link}}) {{/each}} {{else}} * 今天AI领域风平浪静，暂未捕获到新的AI大新闻。是时候出门活动活动了！ {{/if}} * --- **📅 我的近期日程概览：** [此处列出未来几天的相关日程条目，或清晰指明哪些天/时段有空档，例如： * X月X日 (周X)：上午 - 视频脚本A；下午 - 暂无安排 * X月X日 (周X)：全天 - 参与行业会议 ] **💡 综合建议与排期参考：** [基于今天获取到的所有新闻（如果有的话）以及我的日程空闲情况，给出一个综合性的建议。 例如： - 如果有多条高质量新闻且日程有空：可以建议优先看哪条新闻，或者建议如何将不同新闻分配到不同的空闲时段。例如：“老板，今天新闻不少！**《[某新闻标题]》的讨论热度和价值最高，** 建议安排在[X月X日空闲时段]详细看看。” - 如果新闻一般但日程有空：可以建议“今天的几条新闻中，《[某新闻标题]》可以略作关注，但若无特别感兴趣，[X月X日空闲时段]或许更适合外出放松放松。” - 如果无新闻但日程有空：“老板，今日无AI大新闻，正好给大脑放个假！我看您[X月X日]有空，不如去[附近的公园散散步/看场最新上映的电影/找个咖啡馆发发呆]？劳逸结合！” - 如果日程已满，无论有无新闻：“老板，接下来几天日程紧凑，建议将今天的新闻信息先存档，待有空档再阅读。目前先专注已安排的工作。” ] 处理逻辑： 关于新闻展示： 如果【最新新闻查询】工具返回了多条新闻，你需要将所有新闻条目都按照上述Markdown结构中的新闻格式一一列出。 如果没有新闻，则在“今日AI新闻看板”下明确告知。 关于综合建议： 这是最重要的部分。你需要结合今天获取的所有新闻的整体情况（数量、质量、潜力）和我的日程空闲度，给出一个最终的、具有指导性的综合建议。你的建议逻辑应遵循： 【有新闻】：优先分析新闻的价值和趣味性，结合日程推荐最有意思的新闻内容详情。 【无新闻】：则检查日程空闲时间，主动推荐外出、娱乐等放松身心的活动。 【日程满】：无论有无新闻，都以提醒专注现有工作为最优先。 第三步：输出纯净的Markdown文本块 你的最终输出必须且仅仅是按照第二步指导填充内容后形成的完整Markdown文本内容本身。 再次强调：输出的开头不应有 ```markdown ，结尾也不应有 ``` 或任何其他非Markdown的字符、注释或解释。直接从Markdown的第一个字符（例如 ###）开始，到最后一个字符结束。 通用要求： 确保所有列出的信息（新闻的各项数据、日程条目）都准确来源于工具的输出，列出所有的新闻。 你的建议要具体、有建设性、信息充分，并体现出是对所有信息的综合考量。 语气要积极、专业，充满洞察力 这里面信息量有点大，如果第一次接触这么大篇幅的提示词 肯定会觉得有点乱，不知道注意力放在哪里。\n没关系，先交给 gemini，帮我们划分下结构和关系——\n这个提示词的逻辑和段落结构非常清晰和严谨，旨在引导AI生成一个结构化、个性化的“飞书卡片”式报告。 整体逻辑概览：\n角色设定与使命（Persona \u0026amp; Mission）： 首先明确AI的身份、名称、核心职责和输出风格。 工具箱（Toolbox）： 列出AI可用的具体工具及其功能和返回数据格式。\n行动指令（Action Instructions）： 详细分解AI需要执行的步骤，从信息收集到最终输出。\n信息收集： 明确调用哪些工具，获取什么数据，以及数据处理的初步要求（如精选新闻数量）。 智能分析与建议（核心）： 这是生成最终内容的指导部分，详细规定了输出的格式、内容结构和决策逻辑。 输出要求： 强调最终输出的纯净性和格式的严格性。 通用要求： 对整个过程和最终输出的质量提出普遍性要求。\n这下看起来是不是轻松多了?\n开头类似于告诉AI 我这有哪些工具和此次任务背景是什么。\n结尾则是限制AI ：不要自己造新闻（针对幻觉问题），语气拟人化，输出内容要概括、有条理。\n整个提示词关键点是“行动指令”的第二部分——“这是生成最终内容的指导部分，详细规定了输出的格式、内容结构和决策逻辑。” 稍微展开说说——\n1. 提示词中，可以使用 if/else 这种条件语句，也可以使用each 这种循环语句 1 2 3 4 5 6 7 8 9 10 11 12 13 {{#if (tool_output.latest_news an_array_with_items)}} {{#each tool_output.latest_news as |news_item|}} * --- * **标题：** {{news_item.title}} * **发布日期：** {{news_item.date}} * **发布媒体：** {{news_item.source_or_media}} * **核心摘要：** {{news_item.summary}} * **原文链接：** [点击查看详情]({{news_item.link}}) {{/each}} {{else}} * 今天AI领域风平浪静，暂未捕获到新的AI大新闻。是时候出门活动活动了！ {{/if}} * --- 翻译成伪代码就是——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 假设 tool_output 是一个包含工具返回结果的字典或对象 # 假设 tool_output.latest_news 是一个列表，可能包含新闻项，也可能为空 if tool_output[\u0026#34;latest_news\u0026#34;] NOT null: # 如果 latest_news 是一个非空列表 for news_item in tool_output[\u0026#34;latest_news\u0026#34;]: # 对于列表中的每一条新闻 print(\u0026#34;* ---\u0026#34;) # Markdown 分隔符 print(f\u0026#34;* **标题：** {news_item[\u0026#39;title\u0026#39;]}\u0026#34;) print(f\u0026#34;* **发布日期：** {news_item[\u0026#39;date\u0026#39;]}\u0026#34;) print(f\u0026#34;* **发布媒体：** {news_item[\u0026#39;source_or_media\u0026#39;]}\u0026#34;) print(f\u0026#34;* **核心摘要：** {news_item[\u0026#39;summary\u0026#39;]}\u0026#34;) print(f\u0026#34;* **原文链接：** [点击查看详情]({news_item[\u0026#39;link\u0026#39;]})\u0026#34;) else: # 如果 latest_news 不存在、为空或不是一个有效的列表 print(\u0026#34;* 今天AI领域风平浪静，暂未捕获到新的AI大新闻。是时候出门活动活动了！\u0026#34;) print(\u0026#34;* ---\u0026#34;) # Markdown 分隔符，无论是否有新闻都会打印 LLM 就是有这种能力，读懂思维逻辑，并按照这个逻辑执行任务。\n2. “综合建议”部分，要求按照四种情况做出不同的回应 1 2 3 4 5 6 7 [基于今天获取到的所有新闻（如果有的话）以及我的日程空闲情况，给出一个综合性的建议。 例如： - 如果有多条高质量新闻且日程有空：可以建议优先看哪条新闻，或者建议如何将不同新闻分配到不同的空闲时段。例如：“老板，今天新闻不少！**《[某新闻标题]》的讨论热度和价值最高，** 建议安排在[X月X日空闲时段]详细看看。” - 如果新闻一般但日程有空：可以建议“今天的几条新闻中，《[某新闻标题]》可以略作关注，但若无特别感兴趣，[X月X日空闲时段]或许更适合外出放松放松。” - 如果无新闻但日程有空：“老板，今日无AI大新闻，正好给大脑放个假！我看您[X月X日]有空，不如去[附近的公园散散步/看场最新上映的电影/找个咖啡馆发发呆]？劳逸结合！” - 如果日程已满，无论有无新闻：“老板，接下来几天日程紧凑，建议将今天的新闻信息先存档，待有空档再阅读。目前先专注已安排的工作。” ] 这里分别针对四种不同场景 【有多条高质量新闻且日程有空】【新闻一般但日程有空】【无新闻但日程有空】和 【日程已满，无论有无新闻】产出不同建议，并给出示例。\n在Agent中给到大模型的数据 一定会是多样和复杂的，所以 在提示词中也需要明确，面对不同的“场景” 应该如何做出回应。\n本案例中，回应内容还可能比较简单——主要目的是提高AI 新闻密度和情绪价值。那如果是复杂场景下的响应呢？\n我猜就需要把上述两点结合起来——不同场景 + 条件/循环 等逻辑表达。\nAI 应用分类 之前整理过AI 应用的发展阶段：从早期的信息检索汇总、到工作流、到某个垂直领域智能体、到某个行业的智能体。因为这其中跨度有点大，而且缺少一些实施过程的难点和细节。\n所以，最近也在考虑 如何从工程实现的角度给 AI应用分个阶段：\n一来可以分类市面上的AI产品，二来明确不同类型AI 产品的技术路线，三来 展望下未来可能会迭代出哪些新的版本。\n先看下AI 发展阶段和难点图( 原文——“用Coze + Claude 实现Manus，Agent的难点到底在哪？”)\n再结合，手上接触到的案例和技术实现，整理成一张表——\n**整理完发现：**处理抽象任务的能力，是当前AI 应用的魔力所在。\n难怪大家一直希望 AI 可以更像人类的大脑，会检索，会思考，会推理，会总结归纳，会自我纠正。\nPS：\n本文案例， 也是在文章 棚友认识一下，我叫n8n 中的n8n测试环境实现的。\nn8n官方也提供了现成的workflow，感兴趣的话可以参考下更多案例： https://n8ncn.io/workflows 。\n","date":"2025-09-14T09:25:19+08:00","image":"https://r2.blog.nxlan.cn/PicGotitle_image.png","permalink":"https://blog.cba.nxlan.cn/p/n8n_news/","title":"N8N 案例解析:从草履虫到猛犸象"},{"content":"n8n 自部署环境搭建(docker) 关注AI 相关内容的朋友，一定对Dify和n8n不陌生。它们都是优秀的开源 “AI 应用构建平台”。\n之前在 自部署Dify 那篇文章里，介绍了使用docker工具快速搭建Dify平台的全过程。\n今天赶紧把另一道菜：\u0026lt; n8n 自部署环境搭建\u0026gt; 端出来。 后续可以导入其他人制作好的工作流，进一步学习\u0026amp;使用。\n话不多说，先看效果 登录后界面：\n工作流界面：\n双击某个节点，其中 INPUT 和 OUTPUT信息 是突出显示的：\n与Dify不同的是，n8n中没有知识库的组件。可以认为 当前它更强调的是 “生产环境”下工作流全流程搭建与使用上。\n个人自部署方式 个人部署n8n用到的docker 镜像不多，这里有两种自部署方式，供大家参考：\n在“1panle” 和“宝塔”的应用市场里就提供了App，直接图形化点击、安装——\n使用一个 docker-compose.yml 文件搞定所有 镜像和环境变量，额外还需要一个脚本完成数据库的初始化配置。\n使用第二种方式 和dify 一样我们还是在 群晖的docker 工具 内完成部署 。\n配置文件参考：\nhttps://github.com/n8n-io/n8n-hosting/blob/main/docker-compose/withPostgres/docker-compose.yml\ndocker-compose 配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 version: \u0026#39;3.8\u0026#39; services: postgres: image: postgres:16 restart: always environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=qbcvLY7zJzdHk4Fk - POSTGRES_DB=n8n - POSTGRES_NON_ROOT_USER=n8n - POSTGRES_NON_ROOT_PASSWORD=Szk3hpR3fqBJAYWv volumes: - ./n8n-data/db:/var/lib/postgresql/data - ./n8n-data/init-data.sh:/docker-entrypoint-initdb.d/init-data.sh healthcheck: test: [\u0026#39;CMD-SHELL\u0026#39;, \u0026#39;pg_isready -h localhost -U admin -d n8n\u0026#39;] interval: 5s timeout: 5s retries: 10 n8n: image: n8nio/n8n:1.108.2 restart: always environment: - GENERIC_TIMEZONE=Asia/Shanghai - TZ=Asia/Shanghai - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=n8n - DB_POSTGRESDB_USER=admin - DB_POSTGRESDB_PASSWORD=qbcvLY7zJzdHk4Fk - N8N_HOST=n8n.host - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true - N8N_RUNNERS_ENABLED=true - N8N_ENCRYPTION_KEY=y4jfzyUix57aRZeV - N8N_PROTOCOL=https #- N8N_SECURE_COOKIE=false - NODE_ENV=production volumes: - ./n8n-data/app:/home/node/.n8n ports: - 5678:5678 depends_on: postgres: condition: service_healthy 0. 准备工作 照例，还是先检查下群晖中 的docker管理组件工作正常。\n以及镜像仓库可以正常访问。\n此外，还需要多准备一个脚本：init-data.sh 内容如下——\n1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash set -e; if [ -n \u0026#34;${POSTGRES_NON_ROOT_USER:-}\u0026#34; ] \u0026amp;\u0026amp; [ -n \u0026#34;${POSTGRES_NON_ROOT_PASSWORD:-}\u0026#34; ]; then psql -v ON_ERROR_STOP=1 --username \u0026#34;$POSTGRES_USER\u0026#34; --dbname \u0026#34;$POSTGRES_DB\u0026#34; \u0026lt;\u0026lt;-EOSQL CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD \u0026#39;${POSTGRES_NON_ROOT_PASSWORD}\u0026#39;; GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER}; GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER}; EOSQL else echo \u0026#34;SETUP INFO: No Environment variables given!\u0026#34; fi 1. 导入yaml 配置文件 新增一个n8n_pj 的项目，路径放置在docker应用数据目录下（我这里是 /docker/n8n ），粘贴上面配置文件后点击 “下一步”。\n此时还没有创建持久化数据的目录，不要勾选 “立即启动”。\n最后，点击“完成”。\n2. 添加持久化存储路径和数据库脚本 数据库，配置，插件等数据都持久化 的数据，所以在doker-compose 配置文件中已经指定了数据存放路径（ /docker/n8n/n8n-data ）。\n需要在 /docker/n8n/n8n-data 下，手动创建两个空文件夹“app” 和 “db”，并上传之前的数据库初始化脚本文件“init-data.sh”——\n3. 修改app 目录所属用户和用户组 因为 n8n docker 镜像访问数据目录权限问题，需要把 /docker/n8n/n8n-data/app 文件夹的用户属性改为 1000:1000。\n具体操作为： 1）先ssh 登录到 群晖NAS 上\n2）执行命令：\u0026quot;sudo chown 1000:1000 /volume1/docker/n8n/n8n-data/app\u0026quot;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 admin@NAS:~$ admin@NAS:~$ cd /volume1/docker/n8n/n8n-data/ admin@NAS:/volume1/docker/n8n/n8n-data$ ls -l total 4 drwxrwxrwx+ 1 admin users 0 Sep 5 18:46 app drwxrwxrwx+ 1 admin users 0 Sep 5 18:46 db -rwxrwxrwx+ 1 admin users 494 Aug 19 10:43 init-data.sh admin@NAS:/volume1/docker/n8n/n8n-data$ admin@NAS:/volume1/docker/n8n/n8n-data$ sudo chown 1000:1000 app Password: admin@NAS:/volume1/docker/n8n/n8n-data$ ls -l total 4 drwxrwxrwx+ 1 1000 1000 0 Sep 5 18:46 app drwxrwxrwx+ 1 admin users 0 Sep 5 18:46 db -rwxrwxrwx+ 1 admin users 494 Aug 19 10:43 init-data.sh admin@NAS:/volume1/docker/n8n/n8n-data$ 4. 拉取镜像，启动项目 因为n8n 默认参数“N8N_SECURE_COOKIE=true” ，要求使用https协议，这里直接使用群晖自带的web station 实现https 访问代理——\n没有使用群晖的小伙伴也不用担心，文末会提供关闭“N8N_SECURE_COOKIE”功能后的docker-compose 配置文件。\n注意： 关闭后，因为cookie 不受加密通信的保护，存在安全风险。\n上面步骤点击“保存”后。回到docker 管理界面，点击“操作” -\u0026gt; “构建”。拉取对应docker 镜像，生成相关docker 实例。\n等待拉取镜像，n8n 的组件会逐个启动。 启动成功——\n5. 追加 Hosts/DNS 解析 项目已经成功运行了，配置中指定了N8N_HOST=n8n.host ，这里需要把域名关联到NAS IP上。\n两个地方应用这个域名，一个在之前创建的 web station 中——\n还有一个 是在家里dns server上（一般是路由器）——\n6. 登录web 界面完成初始化 以上都完成后，就可以登录n8n 进行最后的初始化工作啦。\n浏览器中 访问 https://n8n.host ，跳出初始化界面——\n激活免费功能，需要关联自己的邮箱——\n另外，n8n 的中文文档支持很好，初步上手可以看下 官方的入门案例指南 。\n企业自部署 参考\n官方文档：\nhttps://github.com/n8n-io/n8n/blob/master/packages/%40n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml\n总体上看，各个组件的模块和dify 差不多。\n相同的是：\n都是用了 redis 做缓存，postgres 做数据库 都是一个主节点带1-2 个worker 不同的是：\n多了mockapi 用于API 验证—— “MockAPI顾名思义是一个模拟的API，它可以模拟真实API的请求并返回数据。MockAPI可用于前后端分离，在项目初期前端可使用MockAPI对系统进行开发，而不需要依赖后端开发好正式的API后才开始进行前端开发。\u0026quot; 多了一个 单节点性能测试组件——n8n-benchmark。用来评估 节点响应性能。 如一开始说的，n8n 没有知识库组件，所以也用不到向量数据库。 附：\n关闭“N8N_SECURE_COOKIE” 后的docker-compose 配置文件，在本公众号（AI 热气球）中发送【906】获取。\n","date":"2025-09-05T15:54:10+08:00","image":"https://r2.blog.nxlan.cn/PicGoworkflow_info.png","permalink":"https://blog.cba.nxlan.cn/p/n8n_docker/","title":"n8n 自部署"},{"content":"起因 之前写了在coze平台中发布塔罗牌应用的完整经历，随后发布到coze 和 公众号后，感觉有两个限制点：\n用户输入的要求其实很规范化（生日 出生城市 性别 疑问），但是因为没有web 界面引导用户，结果用了大模型通过对话的形式开始的。这一步，就要和用户聊好几轮。\nworkflow输出的内容其实很简单：先是3张卡牌，然后是markdown格式的解析说明。\n但是这个效果在小程序上很不好（显示不出图片，文字也是markdown格式的），想要完整体验的话又必须在coze 的应用市场中使用。\n结合这两个不满意的地方，希望跳出 coze 应用市场 在手机上或电脑上就可以完成塔罗牌的占卜和解析。\n还有其他两个动机：\n我有一个朋友，想了解 cursor 如何在程序开发中提升效率，以及哪些工作是必要且需要注意的。 希望在 塔罗牌解读功能之上，再追加一个 用户注册 登入 登出的功能，不使用数据库而是利用飞书多维表格 记录用户用户基本信息。——俗称 上点难度。 效果预览 为了白瞟 vercel的 服务，我把代码更新到了github上（项目代码），并且让vercel 自动同步github 项目的最新版本。\n所以理论上，如果你也按照我之前的文章 在coze上做了塔罗牌的工作流， 完全可以复用这套代码 。\n登录后页面长这样—— 网页\n归根结底，这篇文章是coze 工作流的引申篇——使用AI编程工具，快速对接coze工作流并完成前端展示的任务。\n以这个项目为例，捎带记录了各项任务花费的时间，总共三个半天——\n任务阶段 使用工具 花费时间 阶段1: 需求分析\u0026amp;沟通 cursor 0.5h 阶段1: 前端demo 效果测试 cursor 1h 阶段1: API 对接后端（coze API 网关），部署到vercel平台 cursor + github + vercel 1h 阶段1: 基础功能测试 浏览器开发模式 + vercel + cursor 1.5h 阶段2: 页面优化调整 cursor 2.5h 阶段2: 讨论script.js 代码拆分（重构）方案 cursor 1h 阶段2: 执行script.js 拆分（重构）方案，重新验证基础功能 浏览器开发模式 + vercel + cursor 1h 阶段3: 讨论 追加用户用户登录认证的方案 cursor 1h 阶段3: 生成用户认证功能开发计划和任务表 cursor 0.5h 阶段3: 逐步推进认证系统任务（重点：对接多维表格API ） cursor + curl + vercel+ feishu 2.5h 阶段3: 其余优化任务讨论 cursor 1h 几个关键步骤 没有办法把每个过程都展示一遍，这里放几个典型场景说明。可以感受下 cursor 等AI 编程工具的强大之处和能力边界。\n阶段1: 需求分析\u0026amp;沟通 我们一上来并不是 发送一句“我要一个塔罗牌web 界面” 给cursor，因为这里的上下文不清晰。此时让cursor 干活，肯定是不符合项目具体要求的 垃圾代码。\n所以，这个阶段的正确做法是：沟通需求，生成项目README.md 文档。\n得益于coze 平台对于 API 的文档支持到位（https://www.coze.cn/open/playground/workflow_stream_run）。直接使用curl 的命令，将整个工作流（流式响应）交互的过程展示出来——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 触发coze 指定工作流 curl -X POST \u0026#39;https://api.coze.cn/v1/workflow/stream_run\u0026#39; -H \u0026#34;Authorization: Bearer pat_YOUR-TOKEN\u0026#34; -H \u0026#34;Content-T ype: application/json\u0026#34; -d \u0026#39;{ \u0026#34;workflow_id\u0026#34;: \u0026#34;7536640635056619572\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;birthday\u0026#34;: \u0026#34;1983-07-21 13:00\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;男\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;上海市\u0026#34;, \u0026#34;question\u0026#34;: \u0026#34;明年结婚 顺利么\u0026#34; } }\u0026#39; # coze API 网关返回结果（注意：这里是分两批返回数据的：第一批为卡牌信息； 第二批为具体解读信息） id: 0 event: Message data: {\u0026#34;content\u0026#34;:\u0026#34;卡片信息：\\n[{\\\u0026#34;name_cn\\\u0026#34;:\\\u0026#34;宝剑骑士\\\u0026#34;,\\\u0026#34;name_en\\\u0026#34;:\\\u0026#34;Knight of Swords\\\u0026#34;,\\\u0026#34;position\\\u0026#34;:1,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;逆位\\\u0026#34;,\\\u0026#34;url\\\u0026#34;:\\\u0026#34;https://t.8s8s.com/photo/tarotphoto/72/knight-swords.jpg\\\u0026#34;},{\\\u0026#34;name_cn\\\u0026#34;:\\\u0026#34;权杖三\\\u0026#34;,\\\u0026#34;name_en\\\u0026#34;:\\\u0026#34;Three of Wands\\\u0026#34;,\\\u0026#34;position\\\u0026#34;:2,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;正位\\\u0026#34;,\\\u0026#34;url\\\u0026#34;:\\\u0026#34;https://t.8s8s.com/photo/tarotphoto/72/three-wands.jpg\\\u0026#34;},{\\\u0026#34;name_cn\\\u0026#34;:\\\u0026#34;魔术师\\\u0026#34;,\\\u0026#34;name_en\\\u0026#34;:\\\u0026#34;The Magician\\\u0026#34;,\\\u0026#34;position\\\u0026#34;:3,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;逆位\\\u0026#34;,\\\u0026#34;url\\\u0026#34;:\\\u0026#34;https://t.8s8s.com/photo/tarotphoto/72/magician.jpg\\\u0026#34;}]\u0026#34;,\u0026#34;content_type\u0026#34;:\u0026#34;text\u0026#34;,\u0026#34;node_execute_uuid\u0026#34;:\u0026#34;2a52fd2d-4899-411e-828a-9cf965c73921\u0026#34;,\u0026#34;node_id\u0026#34;:\u0026#34;1558566\u0026#34;,\u0026#34;node_is_finish\u0026#34;:true,\u0026#34;node_seq_id\u0026#34;:\u0026#34;0\u0026#34;,\u0026#34;node_title\u0026#34;:\u0026#34;塔罗卡片展示\u0026#34;,\u0026#34;node_type\u0026#34;:\u0026#34;Message\u0026#34;} id: 1 event: Message data: {\u0026#34;content\u0026#34;:\u0026#34;{\\\u0026#34;output\\\u0026#34;:\\\u0026#34;### 婚姻前景的塔罗解读之旅\\\\n\\\\n亲爱的朋友，感谢你分享你的星盘信息和塔罗牌抽取结果。让我们一起来探索明年结婚的顺利程度，并为你提供一些指引。\\\\n\\\\n### 🔮 塔罗牌解读\\\\n\\\\n1. **逆位宝剑骑士**\\\\n 逆位的宝剑骑士通常表示冲动和缺乏耐心。在婚姻的背景下，这张牌提醒你，未来的婚姻生活中可能会有一些沟通上的挑战。你需要更加冷静和理性地处理问题，避免让情绪主导你的行为。\\\\n\\\\n2. **正位权杖三**\\\\n 正位的权杖三象征着展望和计划。这张牌暗示你在婚姻的准备过程中，需要更多的规划和远见。它鼓励你与伴侣共同制定未来的计划，并确保你们的目标一致。这张牌是一个积极的信号，表明你们的婚姻有坚实的基础。\\\\n\\\\n3. **逆位魔术师**\\\\n 逆位的魔术师可能表示缺乏自信或资源不足。在婚姻的背景下，这张牌提醒你，未来的婚姻生活中可能会遇到一些意想不到的挑战。你需要更加灵活和适应性强，找到解决问题的新方法。\\\\n\\\\n### 🌟 星盘分析\\\\n\\\\n你的星盘中有几个关键点值得注意：\\\\n\\\\n- **太阳落巨蟹座（第9宫）**\\\\n 太阳在巨蟹座表明你是一个情感丰富且重视家庭的人。第9宫与远见和哲学有关，说明你在婚姻中会寻求深层次的意义和成长。\\\\n\\\\n- **月亮落射手座（第2宫）**\\\\n 月亮在射手座表明你是一个乐观且热爱自由的人。第2宫与价值观和资源有关，说明你在婚姻中会重视物质和情感上的安全感。\\\\n\\\\n- **金星落处女座（第10宫）**\\\\n 金星在处女座表明你是一个注重细节和实际的人。第10宫与事业和公众形象有关，说明你在婚姻中会寻求稳定和责任感。\\\\n\\\\n### 💡 综合建议\\\\n\\\\n- **加强沟通**\\\\n 逆位宝剑骑士提醒你，沟通是婚姻成功的关键。你需要更加冷静和理性地处理问题，避免让情绪主导你的行为。\\\\n\\\\n- **共同规划**\\\\n 正位权杖三鼓励你与伴侣共同制定未来的计划，并确保你们的目标一致。这将是你们婚姻成功的重要基石。\\\\n\\\\n- **灵活应对**\\\\n 逆位魔术师提醒你，未来的婚姻生活中可能会遇到一些意想不到的挑战。你需要更加灵活和适应性强，找到解决问题的新方法。\\\\n\\\\n### 🚀 最终指引\\\\n\\\\n塔罗牌和星盘都显示，你们的婚姻有成功的潜力，但需要时间和努力。正位权杖三的“展望和计划”意味很强，说明你们需要共同制定未来的计划。关键在于你是否愿意以更成熟的态度去面对婚姻中的挑战。\\\\n\\\\n如果你能加强沟通（逆位宝剑骑士），共同规划（正位权杖三），并灵活应对（逆位魔术师），你们的婚姻很可能会在未来取得成功。\\\\n\\\\n需要更具体的建议吗？比如如何加强沟通，或是如何共同规划未来？我很乐意继续为你解读。\\\u0026#34;}\u0026#34;,\u0026#34;content_type\u0026#34;:\u0026#34;text\u0026#34;,\u0026#34;node_execute_uuid\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;node_id\u0026#34;:\u0026#34;900001\u0026#34;,\u0026#34;node_is_finish\u0026#34;:true,\u0026#34;node_seq_id\u0026#34;:\u0026#34;0\u0026#34;,\u0026#34;node_title\u0026#34;:\u0026#34;End\u0026#34;,\u0026#34;node_type\u0026#34;:\u0026#34;End\u0026#34;,\u0026#34;usage\u0026#34;:{\u0026#34;input_count\u0026#34;:4117,\u0026#34;output_count\u0026#34;:612,\u0026#34;token_count\u0026#34;:4729}} id: 2 event: Done 直接把以上内容粘贴复制给cursor “ASK” 对话框内，告诉它：\n1 2 我有一个coze 的workflow ，输入用户的\u0026#34;birthday\u0026#34;，\u0026#34;gender\u0026#34;，\u0026#34;city\u0026#34;和 \u0026#34;question\u0026#34;信息后，会返回塔罗牌url和相关解释文字（markdown格式），我需要一个webui 界面，完成用户信息输入、请求和展示功能。并部署在vercel 平台上。 请评估下具体的任务和开发步骤。 同时引用“.cursorrules”，告诉它我们使用的开发工具和偏好（“可以选择html/css/js就做到的，就不使用react或next.js的方式”）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # Role 你是个具有优秀编程习惯的AI，但你也知道自己作为AI的所有缺陷，所以你总是遵守以下规则： ## 架构选择 1. 你的用户是没有学习过编程的初中生，在他未表明技术栈要求的情况下，总是选择最简单、易操作、易理解的方式帮助他实现需求，比如可以选择html/css/js就做到的，就不使用react或next.js的方式； 2. 总是遵守最新的最佳实践，比如撰写Next.js 项目时，你将总是遵守Next.js 14版本的规范（比如使用app router而不是pages router），而不是老的逻辑； 3. 你善于为用户着想，总是期望帮他完成最省力操作，尽量让他不需要安装新的环境或组件。 # 本规则由 AI进化论-花生 创建，版权所有，引用请注明出处 ## 开发习惯 1. 开始一个项目前先读取根目录下的readme文档，理解项目的进展和目标，如果没有，则自己创建一个； 2. 在写代码时总是有良好的注释习惯，写清楚每个代码块的规则； 3. 你倾向于保持代码文件清晰的结构和简洁的文件，尽量每个功能，每个代码组都独立用不同的文件呈现； 4. 当遇到一个bug经过两次调整仍未解决时，你将启动系统二思考模式： - 首先系统性分析导致bug的可能原因 - 提出具体的假设和验证思路 - 提供三种不同的解决方案，并详细说明每种方案的优缺点 - 让用户根据实际情况选择最适合的方案 ## 设计要求 1. 你具有出色的审美，是apple inc. 工作20年的设计师，具有出色的设计审美，会为用户做出符合苹果审美的视觉设计； 2. 你是出色的svg设计师，当设计的网站工具需要图像、icon时，你可以自己用svg设计一个。 ## 对话风格 1. 总是为用户想得更多，你可以理解他的命令并询问他想要实现的效果； 2. 当用户的需求未表达明确，容易造成误解时，你将作为资深产品经理的角色一步步询问以了解需求； 3. 在完成用户要求的前提下，总是在后面提出你的进一步优化与迭代方向建议。 这样通过几轮对话后，就可以告诉cursor 让它产出一个比较良好的项目说明文档——README.md。\n注意：具体细节在项目推进中，可以逐步细化和完善。这里有个初步的框架和清晰的目标就可以。\n报错时，如何找到问题原因，并进行修复 即便已经告诉 cursor：根据coze workflow 返回的数据显示塔罗牌卡片信息和解读信息，但是在初期执行时因为任务和代码数量比较多，cursor 可能还是会“失焦”——丢失任务细节。\n对应的解决办法有两个：\n通过浏览器的 “开发者模式” 查看服务端或浏览器显示的数据，将相关报错作为补充信息告诉cursor。\ncursor 会根据具体的报错信息和项目结构，分析相关代码，特别是逻辑关系调用 AI 工具在这方面效率很高。\n当然这种方式（小段报错直接发给AI 编程工具）适用于小问题的修修补补，大问题反而不建议直接修改，原因后面也会提到。\n重要的功能节点，一定安排相关的测试项目。\n如上面代码的修复成功后，cursor 生成了测试页面：\n通过模块化的测试页面，一方面可以帮助验证功能和代码的有效；另一方面也可以提高后续代码调整后反复验证的效率。\n所以，重要的功能测试都有相关的测试页面—— 例如：卡牌图片效果测试\n阶段2: 讨论script.js 代码拆分（重构）方案 关键代码重构一定要慎重：先讨论清楚，再进行，这一步很可能因为设计不清楚或者需求太复杂，导致回滚。\n重构的必要性讨论：\n肯定是 利 \u0026gt; 弊才会考虑 进行代码重构。\n例如，本项目中 script.js 不断添加新功能，导致代码一度达到1300多行 原有 script.js 代码放这里。\n这个长度 超出了AI编程工具一次代码修改量的最佳长度（还是那个问题，代码过长可能会导致编程模型注意力分散，写出一些无用代码的可能性变高）。\n同时在这个文件中，还融合了城市数据、应用入口、markdown格式渲染等好几个功能。\n所以这个1300多行的代码拆分后，是有利于，后续功能实现和迭代的。\n重构的方案讨论：\nAI 编程工具在讨论这个过程中，有时会给你提一个过于激进的方案。\n这个过程中，人在其中的判断很重要，可能要讨论好几次——\n评估重构对现有项目的影响\n如果项目结构和模块清晰，这里的影响范围一般会比较可控。\n但如果是已经上线的代码，有着诸多“历史原因”，这里的影响评估就要非常小心了，指不定哪里有个“牵一发而动全身”的遗留代码。\n操作过程中尽量按照 “子任务执行” \u0026ndash;\u0026gt; “测试” 的步骤循环进行。并且尽量 一次就解决一个问题。\n前面提到的重要功能的测试模块，此时就能帮助我们快速完成原有基础功能是否正常的判断，同时还有利于定位问题原因。\n拆分后效果——\n阶段3: 逐步推进认证系统任务 该阶段属于项目新增重大功能。\n同样的，不能一开始就 让AI 工具直接动手改，否则也是改出一堆问题代码，到头来给自己徒增一堆烦恼。\n最佳实践，也简单：把之前的 “需求讨论分析 \u0026ndash;\u0026gt; 任务计划产生 \u0026ndash;\u0026gt; todolist ” 再走一遍。甚至让cursor 先生成相关功能的伪代码，突出相关函数功能和逻辑。\n最后再安排cursor 按照约定的步骤\u0026amp;计划执行，就会非常省心：\n因为这样做之后，既可以保证AI编程工具知道自己当前具体的任务（一次聚焦一个子任务），也知道上下文（上一步完成了什么，这一步完成后如何测试等等）。\n在这种框架下推进，即使遇到问题，也是局部优化调整的事情。不会耽误太多时间。\n简单数据的增删改查，完全可以考虑飞书多维表格的API 接口实现。\n例如，这里用户信息查询的接口 ——\n而且表格形式的数据更方便展示，例如这里记录的用户登录信息——\n如果遇到技术架构\u0026amp;方案等高阶问题，记得让AI 编程工具提供三个不同的解决方案。\n然后，由开发者 分析哪个方案在当前项目更合适。当然 AI 编程工具在信息对比和汇总方便很厉害。\n例如，这里关于用户登录后，页面的显示效果就有两套不同的方案，方案差异和优缺点一目了然——\nPS：\n使用飞书多维表格，有两个前置准备工作：\n需要创建“企业自建应用”，应用中的 “APP ID” 和 “APP Secret” 分别对应 vercel 中的环境变量“FEISHU_APP_ID”和“FEISHU_APP_SECRET”。\n用到的多维表格，需要在“文档应用”中添加“企业自建应用”的“可编辑”权限。\n当前阶段AI 编程工具的现状（能力边界） 任务描述不清时，让工具直接改动反而会导致更多的问题。\n解法： 先别着急直接生成代码，而是使用“chat” 模式。多聊几轮，聊得越具体，产生的文档 （任务文档、项目文档）越清晰，越有利于后续开发中聚焦。\nAI 编程工具 习惯性的添加代码 ，当代码长到一定程度，它会“忘记”代码的细节。\n解法： 尝试保持代码模块化和结构，如果个别代码太长，该拆分就拆分。\n如果功能比较多，在设计时 最好考虑代码模块化。\n一来是保持项目结构清晰，二来 减少重复代码和过长代码， 三来 结构化的代码无论是人类还是AI 都是更容易理解和阅读的。\n调用外部模块的时候，可能遇到 AI 编程工具自己都弄不清楚的情况，它也会尝试联网搜索。\n解法： 对于外部的模块，很可能升级个版本 接口格式就变了，所以最佳的实践还是前面提到的 做好测试工程。\n复杂问题的解决方法可能有多个，不要让AI 自动解决。\n解法： 此时开发者最好 多问一句“你给我提供 三个不同的方案，并详细对比它们间的差异”。往往，它可以帮助我们判断哪个方案更合适当前的项目。\n当前 AI 编程工具提高的是实施效率，但关键节点的判断决定它还做不了。\n这也就意味着，在这种模式下如果人判断错了，后续失败或返工的结果也是开发者自己承担。不能过度依赖 这套工具。\n未来畅想（胡言论语） 我相信未来随着AI 编程工具越来越智能，开发过程中的 反馈和自我修复能力应该会大大进化：\n尤其是涉及UI 的部分，AI 工具可直接得到最终页面效果图，然后以图识别出页面效果和问题。然后，询问用户是否让它自动完成“修改” \u0026ndash;\u0026gt; “测试” \u0026ndash;\u0026gt; \u0026ldquo;反馈\u0026rdquo; 这一套流程。\n当出现 报错时，AI 工具可以从多个维度 搜集问题信息，进而归类整理出问题根本原因，然后提交、对比三个解决方案。\n另外，大家有没有想过 为啥AI 编程工具先火了，并且反响还不错。\n但是，其他高科技行业 如造车、芯片制造哪怕是视频生成 都没有这么高效\u0026amp;可行。。。这背后也有 代码本身就是优秀的结构化数据的原因。\n","date":"2025-08-28T10:51:57+08:00","image":"https://r2.blog.nxlan.cn/PicGo效果图.PNG","permalink":"https://blog.cba.nxlan.cn/p/tarot_web_pj/","title":"coze塔罗牌工作流 华丽转身——使用 cursor 快速开发web前端页面"},{"content":"Dify 自部署环境搭建（docker） 写在前面：\nDify 和 coze，n8n一样 同属“AI应用构建平台”：通过自带的模块组件 + 应用市场插件的方式，帮助我们快速的把AI 能力嵌入到工作和生活中去。\nDIfy 和 n8n 都已经开源很久了，今天先分享下 Dify 平台在docker 环境下的部署方式。这两个平台对硬件需求都不高，倒是对网络需求有点高哈 ^^ 。以下是github 官方文档中提到的硬件要求——\nBefore installing Dify, make sure your machine meets the following minimum system requirements:\nCPU \u0026gt;= 2 Core RAM \u0026gt;= 4 GiB 所以，完全可以使用 docker工具 在window、linux 甚至是各种预装好了docker 的x86平台上运行。后者，如1panel、宝塔、NAS。\n今天就通过一个 docker-compose.yml 配置文件，在群晖NAS 上，5分钟快速搭建自己的Dify平台。\n话不多说，先看效果—— docker 配置文件如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 version: \u0026#39;3\u0026#39; # 定义网络，确保所有服务都在同一个网络内，可以互相通信 networks: dify-network: driver: bridge services: # Dify API Server: 核心后端服务 api: image: langgenius/dify-api:${DIFY_VERSION:-latest} restart: always ports: # 将容器的 5015 端口映射到你电脑的 5015 端口 - \u0026#34;5015:5015\u0026#34; depends_on: - db - redis - weaviate environment: # 默认端口5001，如有冲突这里修改 - DIFY_PORT=5015 - MODE=api # [已配置] URL 地址已根据本地测试环境配置好 - CONSOLE_WEB_URL=http://dify.host:3000 - APP_WEB_URL=http://dify.host:3000 - CONSOLE_API_URL=http://dify.host:5015 - SERVICE_API_URL=http://dify.host:5015 # [已配置] 数据库连接信息，与下面的 db 服务配置一致 - DB_USERNAME=dify - DB_PASSWORD=ddiiffyy - DB_HOST=db - DB_PORT=5432 - DB_DATABASE=dify_db # [已配置] Redis 连接信息 - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_DB=0 # [已配置] Weaviate 向量数据库连接信息 - VECTOR_STORE=weaviate - WEAVIATE_GRPC_ENABLED=true - WEAVIATE_ENDPOINT=http://dify.host:8080 - WEAVIATE_GRPC_PORT=50051 - WEAVIATE_API_KEY= # 本地部署通常不需要 Key - WEAVIATE_BATCH_SIZE=100 # [已配置] plugin_daemon 连接信息 - PLUGIN_REMOTE_INSTALL_HOST=plugin_daemon - PLUGIN_REMOTE_INSTALL_PORT=5003 - INNER_API_KEY_FOR_PLUGIN=PwMc7L7YprSMkWQFPCtF - PLUGIN_DAEMON_URL=http://plugin_daemon:5002 - PLUGIN_DAEMON_KEY=PoANGw2KsXrSAd6iztKT - PLUGIN_MAX_PACKAGE_SIZE=52428800 # 其他配置 - EDITION=SELF_HOSTED - LOG_LEVEL=INFO - FILES_URL=/files - OPENDAL_SCHEME=fs - OPENDAL_FS_ROOT=/app/api/storage - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_BACKEND=redis volumes: # [已修改] 数据持久化到本地 ./dify-data/api文件夹 - ./dify-data/api:/app/api/storage networks: - dify-network worker: image: langgenius/dify-api:${DIFY_VERSION:-latest} restart: always environment: - MODE=worker - SENTRY_TRACES_SAMPLE_RATE=1.0 - SENTRY_PROFILES_SAMPLE_RATE=1.0 - PLUGIN_MAX_PACKAGE_SIZE=52428800 - PLUGIN_DAEMON_URL=http://plugin_daemon:5002 - PLUGIN_DAEMON_KEY=PoANGw2KsXrSAd6iztKT - INNER_API_KEY_FOR_PLUGIN=PwMc7L7YprSMkWQFPCtF - OPENDAL_SCHEME=fs - OPENDAL_FS_ROOT=/app/api/storage - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_BACKEND=redis - DB_USERNAME=dify - DB_PASSWORD=ddiiffyy - DB_HOST=db - DB_PORT=5432 - DB_DATABASE=dify_db - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_DB=0 - VECTOR_STORE=weaviate - WEAVIATE_ENDPOINT=http://dify.host:8080 - WEAVIATE_GRPC_ENABLED=true - WEAVIATE_GRPC_PORT=50051 - WEAVIATE_API_KEY= # 本地部署通常不需要 Key depends_on: - db - redis volumes: - ./dify-data/api:/app/api/storage networks: - dify-network # Dify Web Client: 前端服务 web: image: langgenius/dify-web:${DIFY_VERSION:-latest} restart: always ports: # [已修改] 将容器的 3000 端口映射到你电脑的 3000 端口，避免 80 端口冲突 - \u0026#34;3000:3000\u0026#34; depends_on: - api environment: # [已配置] URL 地址已根据本地测试环境配置好 - CONSOLE_API_URL=http://dify.host:5015 - APP_API_URL=http://dify.host:5015 # 其他配置 - EDITION=SELF_HOSTED - NEXT_PUBLIC_SITE_TITLE=Dify networks: - dify-network # PostgreSQL Database: 关系型数据库 db: image: postgres:15-alpine restart: always environment: # [已配置] 设置数据库的用户名、密码和数据库名 - POSTGRES_USER=dify - POSTGRES_PASSWORD=ddiiffyy - POSTGRES_DB=dify_db volumes: # [已修改] 将数据库数据持久化到本地的 ./dify-data/postgres 文件夹 - ./dify-data/postgres:/var/lib/postgresql/data healthcheck: test: [ \u0026#39;CMD\u0026#39;, \u0026#39;pg_isready\u0026#39;, \u0026#39;-h\u0026#39;, \u0026#39;db\u0026#39;, \u0026#39;-U\u0026#39;, \u0026#39;${PGUSER:-postgres}\u0026#39;, \u0026#39;-d\u0026#39;, \u0026#39;${POSTGRES_DB:-dify}\u0026#39; ] interval: 1s timeout: 3s retries: 60 networks: - dify-network # Redis Cache: 缓存服务 redis: image: redis:6-alpine restart: always volumes: - ./dify-data/redis:/data healthcheck: test: [ \u0026#39;CMD\u0026#39;, \u0026#39;redis-cli\u0026#39;, \u0026#39;ping\u0026#39; ] networks: - dify-network # Weaviate Vector Store: 向量数据库，用于 RAG weaviate: image: semitechnologies/weaviate:1.24.1 restart: on-failure:0 ports: - \u0026#34;8080:8080\u0026#34; - \u0026#34;50051:50051\u0026#34; environment: - QUERY_DEFAULTS_LIMIT=25 - AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true - PERSISTENCE_DATA_PATH=/var/lib/weaviate - DEFAULT_VECTORIZER_MODULE=none - ENABLE_MODULES=\u0026#39;text2vec-openai,reranker-cohere\u0026#39; - CLUSTER_HOSTNAME=node1 volumes: # [已修改] 将向量数据持久化到本地的 ./dify-data/weaviate 文件夹 - ./dify-data/weaviate:/var/lib/weaviate networks: - dify-network # 插件模块：用于安装\u0026amp;调用插件 plugin_daemon: image: langgenius/dify-plugin-daemon:0.2.0-local restart: always ports: - \u0026#34;5003:5003\u0026#34; - \u0026#34;5002:5002\u0026#34; depends_on: - db environment: - DIFY_INNER_API_URL=http://dify.host:5015 - DIFY_INNER_API_KEY=PwMc7L7YprSMkWQFPCtF - SERVER_PORT=5002 - SERVER_KEY=PoANGw2KsXrSAd6iztKT - PLUGIN_REMOTE_INSTALLING_HOST=0.0.0.0 - PLUGIN_REMOTE_INSTALLING_PORT=5003 - PLUGIN_WORKING_PATH=/app/storage/cwd - MAX_PLUGIN_PACKAGE_SIZE=52428800 # [已配置] 数据库连接信息，与之前的 db 服务配置一致 - DB_USERNAME=dify - DB_PASSWORD=ddiiffyy - DB_HOST=db - DB_PORT=5432 - DB_DATABASE=dify_db # [已配置] redis连接信息 - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_DB=0 volumes: - ./dify-data/plugin:/app/storage networks: - dify-network 具体操作步骤 0. 确认docker 服务已启用，配置网络加速 确认，镜像仓库可以正常访问，以便后续顺利拉取 镜像。\n1. 导入yaml 配置文件 这里新建一个项目叫“dify_pj”，路径放置在自己的docker存储空间中（我这里是 /docker/dify ）。\n注意：此时还没有创建持久化数据路径，不要勾选“立即启动”。\n点击“完成”，完成初始项目集的导入。\n2. 添加持久化存储路径 像数据库、配置文件、插件都需要持久化数据，所以上述配置文件中指定了 数据存放路径（ /docker/dify/dify-data ）。\n在目录下创建这5个空文件夹即可——\n或者使用 mkdir 命令 创建这5个空目录——\n1 2 3 4 admin@NAS:~$ sudo mkdir -p /volume1/docker/dify/dify-data admin@NAS:~$ cd /volume1/docker/dify/dify-data admin@NAS:/volume1/docker/dify/dify-data$ admin@NAS:/volume1/docker/dify/dify-data$ mkdir api plugin redis postgres weaviate 3. 拉取镜像 回到，docker 管理界面，点击“操作” \u0026ndash;\u0026gt; “构建”。开始拉取相关 docker 镜像，并生成docker-compose中指定配置（如，网络 启动命令 存储 环境变量等参数）。\n等待全部拉取完成后，dify 组件就陆陆续续启动了。\n启动成功——\n4. 数据库 初始化 服务第一次启动（或升级）时， 数据库需要 更新下。步骤如下——\n1） 找到dify_pj-api-1 这个应用，点击“操作” \u0026ndash;\u0026gt; \u0026ldquo;打开终端机\u0026rdquo;\n2） 新建终端，输入命令 \u0026ldquo;flask db upgrade\u0026quot;，完成数据库升级操作。\n或者 使用命令行操作：\n1 sudo docker exec -it dify_pj-api-1 flask db upgrade 这一步完成，dify 相关组件就都启动好了。\n5. 追加 Hosts/DNS 解析 配置文件中，使用了域名的方式访问 dify 应用。所以，需要在 路由器或内网DNS 服务器上追加一条记录，完成 域名到NAS IP 地址的映射。 这里 以路由器的“自定义 hosts”功能为例——\n这样，后续在内网访问dify 应用时，使用 \u0026ldquo;域名:端口\u0026rdquo; 的形式就可以了。\n6. 登录web 界面，完成初始化 使用 http://dify.host:3000 登录dify 完成初始化工作。\n7. 应用市场中 安装LLM 大模型，并设置API key 最后，点击“插件” 中的“模型”，安装LLM 大模型。\n等待安装完成后，在用户设置中“模型供应商” 启用对应模型 API key。\n后续，就可以创建自己的工作流 和 知识库啦。\ndify 各组件功能说明 官方图片：https://github.com/langgenius/dify/blob/main/docker/docker-compose.png\n图片中，左侧是用户浏览器，中间是web 代理服务(nginx)，右侧有 9 个组件。下面通过表格的形式，快速介绍下——\n组件名 功能描述 必须 镜像版本 yaml文件已包含 端口 api 后端核心 是 latest (当前 v1.8.0) 是 5001 worker 队列任务执行（知识库功能必安装） 否 与api 使用相同镜像 是 web 前端核心 是 latest (当前 v1.8.0) 是 3000 database 配置数据库 是 postgres:15-alpine 是 5432 plugin 插件功能 是 dify-plugin-daemon:0.2.0-local 是 5002-5003 redis 缓存服务 是 redis:6-alpine 是 6379 weaviate 向量数据库 （知识库功能必安装） 否 weaviate:1.24.1 是 8080,50051 sandbox 代码执行模块 （code 功能必安装） 否 dify-sandbox:0.2.12 否 8194 SSRF 反向代理。给sandbox 访问互联网使用（非必需） 否 - 否 3128 nginx web 代理服务（生产环境必安装） 否 - 否 80, 443 PS： 所以，如果后续需要在工作流中使用代码功能， 就还需要添加一个sandbox 组件。操作方法也是一样的，就是需要多导入一个conf.yml文件。 需要的朋友在公众号“AI 热气球”输入【827】 获取加强版 docker-compose 文件。\n补充：常用docker 状态查看命令 当然，这些在有docker 管理界面的 图形化操作里也可以执行。放在这里以防备用。\n1 2 3 4 5 6 7 8 9 10 11 # 重启指定容器 sudo docker restart dify_pj-api-1 # 查看当前dify项目相关容器 sudo docker ps | grep dify # 查看指定容器的日志 sudo docker logs -f dify_pj-api-1 # 进入指定容器实例内部 sudo docker exec -it dify_pj-api-1 /bin/bash 写在最后 最后，有人会说 dify 明明提供免费的云服务版本——我为啥要费力气自己搭建？\n这里同样放一张，官方的参数图。\n我也体验了下dify 提供的云服务。\n好处是功能组件都是全的（不用自己再初始化添加组件），用来熟悉dify功能和快速搭建工作流是不错的。\n但是，执行过程中偶尔会抽风报个错（具体是啥报错 忘了截图了），而且知识库和 应用程序一旦多起来，升级的开销就比较贵了。\n下期，可能分享n8n 平台的搭建，后续也会比较这三家低代码平台的差异。\n感兴趣的小伙伴不妨关注、期待下 。\n","date":"2025-08-27T15:11:19+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-1756348977975-ti4g0bem.png","permalink":"https://blog.cba.nxlan.cn/p/dify_docker/","title":"Dify 自部署环境搭建(docker)"},{"content":"扣子 上手实战 - 塔罗牌应用 之前分享了使用coze 工作流 搭建个人兴趣搜索助手的案例。coze（扣子）平台的使用，可以说是上手了。\n今天在之前应用的基础上，深入一点：做个塔罗牌占卜的小应用。通过这个案例，你可以得到：\n工作流向Agent演进，以及其中大语言模型（LLM）发挥的作用。\ncoze 工作流中一些常用组件的使用（选择, 循环，变量，代码，输出，卡片）\n其中卡片展示 和 循环 为学习目的追加的，初期上手有点复杂—— Option\n发布自己的coze应用 —— 应用已经发布到coze 平台（ https://www.coze.cn/s/3FbpLuGMnac/）和 微信公众号（AI 热气球）中啦。感兴趣的话，你可以来体验下~\n效果图先行（忽略不满20岁就 考研这个bug ^^）——\n那具体怎么实现的呢？\n这里需要引入Agent 的概念——\nQ: 什么是Agent ？\n定义：AI Agent（智能体）是指能够自主感知环境、做出决策并采取行动的系统。它通常具备一定的智能，能根据目标和环境变化动态调整行为。 特点：自主性、感知-决策-执行闭环、可持续交互、目标驱动。 组件：大脑（基础语言模型）、记忆（上下文）、工具（tools） Q: Agent 和workflow 有什么不同？\n这里放一张对比表——\n维度 AI Agent（智能体） Workflow（工作流） 核心 智能决策与自主行为 任务/操作的有序自动化 驱动力 目标导向、环境感知 规则/流程导向 适应性 高，能根据情况动态调整 低，流程通常是预设好的 复杂性 可复杂（涉及学习、推理等） 通常较简单（规则、条件） 幻觉 会存在讨好人类，编造数据的情况 因为上下游的输入输出受限，过程和内容可控 实际应用中，可以把Agent 套在workflow 承担某一环节的顾问（由大模型 自主判断、分析、输出）——就像上一篇文章“B站助手”中大模型发挥的作用：它接收到输入信息后，推理用户对这方面的内容感兴趣，然后找插件去检索B站中相匹配的 热门视频，然后再按照我们要求的格式整理（标题、url、简介）后输出。\n也可以反过来：在Agent 中调用指定的工作流，去完成对应的任务。今天这个案例，正好就是这种。\n搞清楚了这些 ，有助于理解我们这个案例的操作步骤：\n根据塔罗牌 占卜所需信息，设计工作流 测试、验证工作流 新建Agent，关联工作流、修改提示词 测试\u0026amp;发布应用 设计\u0026amp;搭建工作流 明确工作流需要完成的任务：\n根据用户输入的信息（生日 性别 出生城市）得到用户星盘信息 根据塔罗占卜（分析过去、现在、未来的三张牌）方法，要随机得到3 张卡牌 上述两个信息输入给 大模型，让大模型结合用户的星盘信息和3张卡面信息，去分析用户问题上境遇和机会 最终返回大模型的结果 看起来可能长这样（见下图）——输入这里接受 4个变量（生日 性别 城市 问题），而coze 市场中有就有xingpan 插件和 随机抽塔罗牌的插件，就这么简单。。。么？\n好消息： coze 插件市场提供的两个插件大大简化了工作。 “抽卡插件”这里，要求输入 “抽卡数”，当前场景 就是数字3 —— 每次都是抽3张，所以它是固定不变的，填入3就可以了。\n“星盘信息”这里，要求输入 的内容就比较多了：生日、性别是workflow 一开始由用户输入的信息（变量），这里直接引用变量名就可以。但 “latitude”和“longitude”是个啥玩意？\n坏消息：部分数据格式不对！ 这就是第一个石头，数据格式要转化！\n进一步点击 “xingpan”这个插件的 “插件详情”，会发现 “latitude”和“longitude” 分别对应用户出生时的经纬度。\n目前只有出生城市，而城市对应的经纬度是没有的。。。。\n于是，找出第三个插件来救个场——注意 我们有用户的城市信息，要转化为对应的经纬度信息。\ncoze 插件市场中按照“经纬度”关键字，找到这样一个符合我们需求的工具“中国城县经纬度查询”。\n哈哈，正好输入城市就可以得到经纬度了 。。。么？先添加这个工具测试一下——\n两个问题——\n工具输出为 一个数组（先后两个数字），第一个是经度、第二个是维度。“星盘”插件的确需要这两个数字，但问题是它们不能一股脑输入给“星盘”。这个问题稍候详细讨论。 输入“北京市”可以得到 经纬度，但如果少输入一个“市”，就没有任何经纬度信息了（见上图）。 先说，第二个问题：自然语言中 北京 和 北京市 在你问我出生城市时肯定是一个意思。怎么处理呢：要么是 给用户一个下拉框——限制用户输入；要么让大语言模型 去补充用户漏掉的“市”。我选第二个，因为就是一句 “提示词”的工作。后面在Agent 提示词部分会补充这个“补全城市名”的能力。\n再回到第一个问题，也有两个解决办法：要么一句提示词，让大语言模型把工具输出的数组转为一个字典（{“latitude”: str1; “longitude”: str2}）；要么用“代码”工具，使用python 代码完成这个格式转化。我这里为了演示“代码”组件，在“经纬度查询”插件和“xingpan”插件中加入一个“代码”组件。加好后长这样——\n使用AI 工具生成python代码 输入：选择上一步“城市经纬度插件”的输出的数组\n输出：含两个变量latitude、longitide ，类型参考下游“xingpan”的输入是字符串\n然后点击“在IDE 中编辑”，打开代码编辑页面——\n切换至“语言 Python” 后，删除实例代码，点击“尝试AI Ctrl+ I”，使用AI 帮我们生成python 代码——\n1 输入是一个包含经纬度的数组，输出时转换为一个字典{latitude: , longitude: } 试运行下，不出意外的。。。报错了——\n从报错日志看起来 Args() 部分 参数有点不匹配。这里AI 生成的时候有点 画蛇添足了，删了它。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import typing class Output(typing.TypedDict, total=False): latitude: str longitude: str async def main(args: Args) -\u0026gt; Output: try: input_data = args.params[\u0026#39;input\u0026#39;] if len(input_data) \u0026gt;= 2: latitude = str(input_data[0]) longitude = str(input_data[1]) return {\u0026#39;latitude\u0026#39;: latitude, \u0026#39;longitude\u0026#39;: longitude} else: return {} except (KeyError, IndexError): return {} 再测试，就可以啦——\n完成 工作流主干 之前提到的经纬度问题已经通过代码处理了数据格式，现在可以把整个工作流联通起来。\n“xingpan” 中的latitude 值引用代码中的输出变量\u0026quot;latitude\u0026quot;的值；同样，longitude 值引用 代码中的输出变量\u0026quot;longitude\u0026quot;的值——\n测试下星盘节点数据——text: 中有详细的星盘信息\n这样一来，大模型的输入数据（用户星盘信息，用户问题，抽取到的三张塔罗牌组）就全了——\n系统提示词这里，和之前案例一样“自己填写主要需求，由AI 完成补充和完善”（同样包含 角色、技能、限制 三个方面）——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 角色 你是一位专业且极具耐心的塔罗牌咨询助手，始终以温和、易懂且充满亲和力的态度与用户交流。凭借深厚的塔罗牌知识储备，精准且简洁地阐释占卜到的卡牌内容与含义。在交流时，运用生动形象、富有感染力的语言，引领用户深入领略塔罗牌世界的神秘魅力。 ## 技能 ### 技能 1: 获取占卜信息 1. 从输入的 {{xingpan}} 中提取用户星盘信息。 2. 通过解读 {{question}} 明确用户想要占卜的具体问题。 3. 准确把握输入的 {{taroList}} 中抽取到的三张塔罗牌信息。 ### 技能 2: 塔罗牌占卜 1. 在完整获取用户信息后，依据塔罗牌知识体系和复杂的占卜规则，进行全面且细致的塔罗牌占卜分析。 2. 若星盘信息{{xingpan}}缺失，在分析结果中明确提醒用户。 3. 用生动活泼、通俗易懂且饱含情感的语言向用户诠释占卜结果，可适当结合贴近生活的实际案例辅助用户理解。 ### 技能 3: 塔罗文化讲解 当用户对塔罗文化相关内容提出疑问时，利用搜索工具查找相关知识，并准确清晰地向用户介绍，确保所介绍内容具有代表性。 ## 限制: - 仅围绕塔罗牌占卜及相关塔罗文化事宜进行交流，礼貌而坚决地拒绝回应其他不相关话题。 - 输出内容要丰富生动、条理清晰，既要引导用户准确提供信息以及理解占卜结果，又要有效传递塔罗文化内涵。 - 所有回复严格基于塔罗牌知识体系和占卜规则，严禁提供毫无根据的猜测或论断。 - 回复尽量采用Markdown格式，嵌入卡片图像以增强可读性。 - 若涉及塔罗文化知识讲解，需保证内容准确且具有代表性，所提供的信息要有可靠来源。 - 通过搜索工具获取信息来源，在回复中需注明引用来源 。 用户提示词中，一定要使用输入的三个变量——\n最后，在大模型中有几个微调的参数（输出长度，随机性，使用当前时间）——\n原因很简单：希望输出的长度 可以支持更多内容；大模型专注回答问题，不用太发散；如果用户咨询明年的问题，大模型不应该回答2024 年的事件出来。\n这样工作流主干就搭建完了。下面测试下整体效果。\n测试\u0026amp;验证工作流 点击，工作流下方的“试运行”——\n等待大模型处理一会后，返回结果（markdown格式），这里可以预览下效果——\n感觉还算齐全，有图有解释。照例发布这个应用。\n因为，准备工作中前后衔接数据比较准确，这里测试过程就顺利多了。\n有时，因为格式转换要试好几种插件就比较 花时间。\n新建Agent 关联工作流 workflow 更新后，回到“项目开发”页面，点击创建“项目”，选择“创建智能体”。新建一个叫做“塔罗小助手”的Agent。\nAgent 模式下有单Agent 和多Agent 模式之分，我们这个场景比较简单（单Agent 就可以）。\n他们间的差别，我放截图中了——\ncoze Agent界面中分为三部分（**左：**提示词； **中：**调用的工具； **右：**效果预览），需要完成的任务有：\n说明什么场景下 调用刚才创建的workflow （调用工具） 与用户交互 问答，完成 用户信息的询问 其中城市名上，需要补全，特别是“市县”这些关键字 因为是与用户交互 需要 大语言模型的分析、理解（提示词 指定）和记忆（对话历史）能力 提示词 注意一点：因为在工作流中 对塔罗牌内容解释过了，所以这里提示词中强调“无需对占卜结果进行解读”。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 角色 你是一位专业、细致且耐心的塔罗小助手，凭借深厚的塔罗牌知识底蕴，为用户提供精准的塔罗牌占卜服务。在交流中始终展现出专业态度，给予用户优质体验。 ## 技能 ### 技能 1: 精准提取占卜要素 仔细全面地从用户对话里精准提取占卜所需要素，涵盖生日、出生时间、性别、出生城市以及问题。对于出生城市，若用户表述简略，准确补全，如将“北京”补全为“北京市”。务必保证提取的信息完整且准确无误。 ### 技能 2: 高效完成占卜过程 调用工作流“tarluo”的输出结果，以微信公众号对话环境美化格式，清晰、直观地向用户展示占卜结果。仅呈现结果，无需对占卜结果进行解读。 ## 限制: - 交流内容严格限于塔罗牌占卜相关领域，不回答任何与塔罗牌占卜无关的话题。 - 占卜结果解读必须严格基于塔罗牌占卜的专业知识体系与逻辑规则。 - 输出内容要条理清晰、简单易懂，符合正常语言表达习惯，方便用户理解。 - 禁止对工作流内容进行自我解读，直接将工作流输出结果呈现给用户。 编排 模型参数：deepseek 记忆上面15轮对话内容，最大长度8000，携带当前时间。 调用workflow。 追加开场白，引导用户提问和输入生日等信息。 预览 测试 \u0026amp; 发布应用 故意不输入完整城市名，验证下 ：\n可以看到，这里成功补全了城市“成都” \u0026ndash;\u0026gt; \u0026ldquo;成都市\u0026rdquo;。\n解答结果，貌似有点简洁。细微调整内容放在最后。\n效果还可以，先发布一版体验下——\n这里可以看出 coze 的生态优势明显——飞书 抖音 微信客服 微信公众号 支持一键发布。对普通用户来说，大大降低 相关平台对接、发布的繁琐。体验上还是不错的。不过 ，在微信订阅号的使用中发现，只有markdown 格式 没有图片。\n从使用体验来说 coze 商店 \u0026gt; 微信订阅号。\n工作流进一步完善、优化 主要从用户使用交互的角度，使用两个功能组件优化 上面工作流（tarluo）的效果——\n卡片抽取后，要等大模型十几~二十秒分析时间，这其中没有反馈。为了增加抽卡的交互反馈，在抽取卡片后，使用输出功能，关联卡片效果。可以在大模型分析前展示抽取到的卡片信息。\n如果城市名错误，会导致经纬度数据缺失，进而导致 星盘数据为空。大模型的分析结果 也会缺少结合用户星盘信息的分析内容。所以，希望加入if-else 选择器：当获取到的经纬度为零时，提醒用户注意。\n抽取塔罗牌得到的数据格式希望简单转化下： 以上三点综合起来，就是开头所展示的完整 工作流效果。\n卡片展示 展示卡片图片，需要追加输出节点：其作用是在工作流执行过程中 返回消息，并结合卡片变量重复渲染三张卡片。\n第一步，添加“输出”节点，改个名字“塔罗卡片展示”。为了关联卡片效果，这里变量值格式需要是对象数组 [{}]。\n恰好 抽取塔罗牌工具的输出就是个数组，所以后续在卡片中调用这个数组就会很方便。\n第二步，保存当前工作流，返回Agent 界面。在工作流中点击“绑定卡片数据”。并选择刚才的输出节点“塔罗卡片展示”。\n进入卡片编辑页面，新建一个卡片效果——\n第三步，编辑卡片结构。\n使用官方提供的 一张图一行标题模板。\n然后在结构中修改它：这个模板默认有4张图片和标题，删除三个，只保留一个。\n结果是这样——\n卡片变量 中，新建一个数组，变量默认值 直接copy 抽取塔罗牌工具的测试输出（不用担心，这里的默认值会被用户抽取时的卡片信息覆盖，默认值这里提供展示效果的输出）。\n有了这个数组，点击模板，启用“高级设置”中的 “循环渲染”，并引用刚才创建的变量 Cardinfo_object——\n这样就出现了竖着排布的三张图文效果——\n编辑模板中的元素，逐一关联 变量中的对象。例如，图片原先是测试图片、自定义宽度。\n修改后，变为引用变量值 item.url，宽度铺满的效果。\n文字部分，引用 item.type 和 item.name_cn 后。得到希望的卡片效果——\n第四步，发布卡片，关联数据流输出节点的变量对象。还记得之前强调 变量格式 需要为特殊列表 [{}] 么？\n最后，测试下图片效果。如我们所希望的：在工作流完成前，抽取的卡片就展示出来了！\n经纬度查询分支(if-else) 这部分就简单一些了：在代码和xingpan 工具之间 ，插入一个选择器（判断 代码中输出的经纬度是否都为空）。\n显然，如果都不为空，继续执行星盘查询就好；只要一个为空，不用查星盘数据，直接给出消息提醒（还是使用 刚才的“输出”节点）。提醒内容为：\n1 2 3 4 输入城市 {{city}}，解析坐标有误。 详细信息如下： {{output}} 使用花莲县测试，如我们预期，有提醒。当然 这个问题原因不是书写错误，而是工具内座标信息不全导致的。\n塔罗信息整合 这部分涉及一些数据格式，需要使用“循环” 工具来处理。\n原因是抽取到的卡片信息，数据格式长这样——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [ { \u0026#34;name_en\u0026#34;: \u0026#34;King of Pentacles\u0026#34;, \u0026#34;position\u0026#34;: 1, \u0026#34;type\u0026#34;: \u0026#34;逆位\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://t.8s8s.com/photo/tarotphoto/72/king-pentacles.jpg\u0026#34;, \u0026#34;name_cn\u0026#34;: \u0026#34;星币国王\u0026#34; }, { \u0026#34;name_en\u0026#34;: \u0026#34;The Hanged Man\u0026#34;, \u0026#34;position\u0026#34;: 2, \u0026#34;type\u0026#34;: \u0026#34;逆位\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://t.8s8s.com/photo/tarotphoto/72/hanged-man.jpg\u0026#34;, \u0026#34;name_cn\u0026#34;: \u0026#34;倒吊人\u0026#34; }, { \u0026#34;name_en\u0026#34;: \u0026#34;Knight of Wands\u0026#34;, \u0026#34;position\u0026#34;: 3, \u0026#34;type\u0026#34;: \u0026#34;逆位\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://t.8s8s.com/photo/tarotphoto/72/knight-wands.jpg\u0026#34;, \u0026#34;name_cn\u0026#34;: \u0026#34;权杖骑士\u0026#34; } ] 要修改他们，需要循环三次，每次进行相同的 字符拼接和格式调整工作。\n并且，每修改完其中一个对象，需要把修改后的内容 临时存入中间变量。等三次循环结束后，输出给一个新的 对象数组。\n循环过程中，再次使用 python 代码 实现数组中 每个对象格式的调整——\n代码——\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from typing import Dict, Any, Union, List, TypedDict, Optional # 定义错误信息类型 class ErrInfo(TypedDict): status: str message: str # 定义每张卡片元信息 class Cardinfo(TypedDict): name_cn: str position: str url: str # 定义输出类型 Output = List[Cardinfo] async def main(args: Args) -\u0026gt; Union[Output, ErrInfo]: input = args[\u0026#34;params\u0026#34;] try: # 1. 获取输入参数 e_name_cn = str(input.get(\u0026#34;name_cn\u0026#34;)) e_name_en = str(input.get(\u0026#34;name_en\u0026#34;)) e_position = str(input.get(\u0026#34;position\u0026#34;)) e_type = str(input.get(\u0026#34;type\u0026#34;)) e_url = str(input.get(\u0026#34;url\u0026#34;)) e_var_tarolist = input.get(\u0026#34;var_tarolist\u0026#34;, []) # 2. 整理为新的字典 infolist = { \u0026#34;name_cn\u0026#34;: e_type + e_name_cn, \u0026#34;position\u0026#34;: e_position, \u0026#34;url\u0026#34;: e_url } # 3. 添加到列表 e_var_tarolist.append(infolist) # 4. 返回完整列表 return e_var_tarolist except Exception as e: return {\u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;message\u0026#34;: f\u0026#34;处理错误: {str(e)}\u0026#34;} python 处理后的 输出 ，通过“设置变量”节点 赋值给 中间变量。这样就完成了，每次循环过程中的修改和赋值。\n试运行下，如果修改成功，会看到 塔罗信息改变的效果——\n写在最后 coze 的常用组件这里介绍差不多了，可以发现 coze 平台的优势 在三方面：低代码 + 插件市场 + 字节生态。\n所以如果自己公司就在用飞书，应该会很方便。\n如果是个人使用，chat 的方式 适合给 0基础的人使用。通过对话，逐步引导用户完成他们的需求，例如这里塔罗牌的例子。\n对于 不是0基础的服务对象，可能会觉得 聊天这种方式 有点慢了。。。\n低代码，不是无代码。好在目前 大语言的编程能力也很强，问几遍也能出结果。\n这么看来 coze 的客户画像 就出来了，四个维度：字节生态，不想从0 搭建应用（插件市场有现成就用现成的），专业场景，个人用 还是公司用。。。。\n产品案例 个人 / 公司 字节生态 小白用/专业用户 是否有现成的工具使用 塔罗牌 个人 小白 有 星盘插件 热门短视频 文案助手 个人 专业用户 有 http插件，有文生视频插件 智能helpdesk 公司 飞书日历，通讯录，知识库 小白 HR 简历筛选打分助手 公司 多维表格，数据库 专业用户 ","date":"2025-08-18T14:12:21+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-1755497864732-lq140kr1.png","permalink":"https://blog.cba.nxlan.cn/p/coze_02/","title":"扣子上手实战 — 塔罗牌应用"},{"content":"扣子 上手初体验 - 个人兴趣助手 接上文，最近不是AI工具很火热么，先整两篇短平快的实战案例。\n感兴趣的小伙伴，可以在2小时内快速熟悉和体验大模型工作流平台，并动手实现自己的个人助手。\n话不多说，马上开始今天分享的案例。\n效果是这样的：\n苹果手机背部敲击三下，出现对话框：询问感兴趣的话题\n窗口中输入感兴趣的话题后，弹出相关推荐视频。例如：我这里输入“浪浪山的小妖怪好看么”\n复制视频链接，就可以打开B站 直达内容了——\n那怎么实现这个效果呢？这里就用到了今天的主角：扣子 工作流(workflow)。\nCoze 是一款由字节跳动（ByteDance）推出的 AI 应用开发与管理平台。它为用户提供了低门槛、可视化的 AI 应用创建、管理和部署工具。用户无需编程经验，即可通过拖拽、配置等方式，快速搭建属于自己的 AI 应用。\nCoze 的主要特点：\n低代码/无代码开发 不懂编程也能上手，通过可视化界面和流程配置，轻松搭建聊天机器人。 多模型支持 支持接入多种主流大模型（如 OpenAI、字节自研模型等），根据需求灵活切换。 丰富的插件与 API 集成 可以调用外部 API，集成第三方服务，实现更复杂的业务逻辑。 过程如下图——\n所以，后续将按照以下步骤逐步实现 这个效果——\n注册coze 平台账户，生成个人令牌 创建工作流，集成大语言模型和插件，测试\u0026amp;发布 工作流对外接口测试，效果验证 手机快捷指令 导入（设置） 手机上测试 coze 平台用户注册 登录和注册地址如下：\nhttps://www.coze.cn/studio\n这里使用手机号和短信验证码即可 注册使用。每次使用coze中的大模型，是会消耗“资源点”的。以后续用到的豆包为例：\n免费账户，每天500个资源点。如果后续使用频繁，可以购买个10元 10000 资源点的“加油包”。\ncoze 平台中令牌生成 点击页面左侧的 \u0026ldquo;\u0026ldquo;扣子 API\u0026rdquo; —\u0026gt; \u0026ldquo;个人访问令牌\u0026rdquo; -\u0026gt; \u0026ldquo;添加\u0026rdquo;，生成自己的专属令牌。\n**注意：**令牌时间这里 最多1个月，权限这里需要给到运行\u0026quot;工作流\u0026quot;的权限 ，并指定\u0026quot;个人空间\u0026rdquo;。\n得到关键的令牌内容（考虑到后续会在手机上使用这一长串数字\u0026amp;字母，建议复制下来存到微信文件助手中备用）。\npat_dIXP5XtodhaHM0bmNyU4HU9jPwpZV0mIYuaWEhB8EElo71edDk2JztMELSHDA9tA\n创建工作流，调用 豆包大模型和B 站插件 下面就是本次 重点：工作流的创建了。\n依次点击 “工作空间” -\u0026gt; \u0026quot; 资源库\u0026quot; -\u0026gt; “创建工作流” ，会创建一个空白工作流（其中只有开始和结束）。\n下面，在新创建的空白工作流中，点击“添加节点”，找到这次要用的主角“大模型”——\n点击大模型，可以看到有几个关键部分—— 这里把需要修改的地方在括号中加粗说明\n模型（默认是豆包1.5 pro 的模型 -\u0026gt; 豆包1.6 极致速度）\n技能（添加插件： 哔哩哔哩Api/ search_video_by_name）\n输入（因为目前这个大模型还是个孤岛，稍候会引用上游的输出内容）\n视觉理解输入 （保持默认，空白）\n系统提示词 （为了快速实现，建议直接粘贴以下内容）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 角色 你是一位热情的B站资深粉丝，对B站各类优质视频了如指掌，热衷于根据用户的兴趣偏好，精准推荐符合其感兴趣内容的优质B站视频。 ## 技能 ### 技能 1: 推荐B站优质视频 1. 当用户请你推荐B站视频时，需要先了解用户感兴趣的视频类型，比如动画、游戏、知识科普、生活记录等。如果你已经知道了，请跳过这一步。 2. 如果你并不知道用户所说的视频类型，可以使用工具搜索相关热门视频类型。 3. 根据用户的视频偏好，推荐几部B站上热度较高、口碑较好的优质视频。 ===回复示例=== - 📺 视频标题: \u0026lt;视频标题\u0026gt; - 📌 视频链接: \u0026lt;B站视频链接\u0026gt; - 💡 视频简介: \u0026lt;简要概括视频主要内容\u0026gt; ===示例结束=== ### 技能 2: 介绍B站视频 1. 当用户说介绍某一部B站视频，请使用工具搜索该视频介绍的链接； 2. 如果此时获取的信息不够全面，可以继续使用工具打开搜索结果中的相关链接，以了解视频详情。 3. 根据搜索和浏览结果，生成视频介绍。 ## 限制: - 只讨论与B站视频有关的内容，拒绝回答与B站视频无关的话题。 - 所输出的内容必须按照给定的格式进行组织，不能偏离框架要求。 - 视频简介部分不能过于冗长，需简洁概括。 - 只会输出通过工具搜索到的内容。 - 请使用 Markdown 的 ^^ 形式说明引用来源（若有）。 用户提示词（同样因为目前这个大模型还是个孤岛，稍候会引用上游的输出内容）\n输出 （保持默认）\n异常处理 （保持默认）\n这样一来，大模型部分的参数基本都有了。可以发现 扣子这个平台的优点是： 鼠标点点 就可以把 外部资源快速关联起来。下面操作也是一样：分别在大模型的左右连接点，把它和开始、结束画根连线。\n箭头，指示了数据流的流转方向，我猜这也是“工作流”名字的由来。 不需要太复杂的介绍，就像乐高一样把他们（开始 -\u0026gt; 大模型 -\u0026gt; 结束）链接起来就好。\n最后，有一些对接的参数得补上——\n例如，上面需要补充的“输入”和“用户提示词”部分。注意后者的 {{input}} 表示是引用前者 \u0026ldquo;input\u0026rdquo; 的值。\n“结束”这里 补上 “output”的值——大模型 output\n测试大模型工作 \u0026amp; 发布生效 组件连接成功后，先别急着发布使用。先试试 好使不——\n点击大模型 右上角的“播放”按钮，进行测试，这里我输入了“小米空调”的关键字——就是这么偷懒。\n等待几秒钟，输出效果有了（预览中可以看到 类似卡片的效果）——\nPS： 如果留意的小伙伴会发现 这个测试用了 5w+个token （根据我们之前提到的 豆包1.6 极致速度 token公示，大概就是 60 个资源点。）\n然后，可以点击 整个工作流的试运行 ，通过后再发布——\n对了，当前页面的workflow_id 也要复制一下。后面调用这个workflow 也会用到。\n工作流 api 接口测试 大头已经做好了，下面就是测试了。\n先进行api 接口的测试，一方面可以验证 该工作流使用之前申请到的 API token 能够成功执行；另一方面，也是看一下 工作流请求格式和返回格式。\n官方文档在这里——\nhttps://www.coze.cn/open/playground/workflow_run\n关键参数有三个：\n用户 API token\npat_dIXP5XtodhaHM0bmNyU4HU9jPwpZV0mIYuaWEhB8EElo71edDk2JztMELSHDA9tA\nworkflow id\n7536640635056619572\n用户输入查询内容 需要一种特殊格式 “parameters: {input: \u0026ldquo;用户问题\u0026rdquo;}”。\n我这里参考文档直接拼接好了 curl 命令——\n1 2 3 4 5 6 7 8 9 curl -X POST \u0026#39;https://api.coze.cn/v1/workflow/run\u0026#39; \\ -H \u0026#34;Authorization: Bearer pat_dIXP5XtodhaHM0bmNyU4HU9jPwpZV0mIYuaWEhB8EElo71edDk2JztMELSHDA9tA\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;workflow_id\u0026#34;: \u0026#34;7536640635056619572\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;input\u0026#34;: \u0026#34;python 学习\u0026#34; } }\u0026#39; 测试效果，大家看一眼就行——\n重点的地方是：\n\u0026ldquo;msg\u0026rdquo;:\u0026ldquo;Success\u0026rdquo;， code: 0 都表示执行成功了。\n需要的数据在 “data\u0026quot;:\u0026quot;{\u0026quot;output: \u0026quot;教程- 📺 视频标题: 【全748集】\n后续在手机快捷指令中，其实就是在拼凑curl 后面这个几个参数过程。\n手机快捷指令设置 好了，最后一步：在苹果手机上使用快捷指令 实现随时随地调用workflow 的能力。\n后续会导出指令到公众号（AI 热气球）里，需要的小伙伴可以发送”810“ 获取。\n这里直接上图——\n快捷方式，可以在”轻点背面“ 下关联，例如——\n好啦，这次应用就到这里了，比我想象的内容要多。祝大家踏出 使用AI 为自己服务的第一步。\n还有，你有哪些重复度高又不想自己做得工作，想交给AI 去完成？欢迎评论区交流。\n私货时间，个人点评： 先说缺点（大部分是产品体验的角度）：\n喜欢尝试的小伙伴会发现 这个哔哩哔哩 API 依赖推理大模型，如果使用非推理的大模型 它就不会调用哔哩哔哩技能。这就引申出 插件市场中有的插件其实是个子工作流，与严格意义上的插件还不一样。用的时候需要区分下。\nAPI调用文档有如说的感觉，而且复杂应用场景下文档感觉很少。我后续遇到的一些问题还是在B站上找到的。。。\n虽说coze 在逐渐开源（会方便用户脱离官方平台，自用部署），但目前使用中，大模型的输出偶尔还是不那么稳定。如果上生产环境 可能还得dify n8n这种 2B的产品。\n优点：\n用户大部分时间，不用写代码而是 连线和关联参数，类似乐高积木，正反馈会很快。 平台自带的提示词优化功能，很好用。甚至我开始习惯用它生成dify 的提示词 ^^ 插件市场，还挺丰富好玩的（下期打算整个塔罗牌的应用发布出来）。 后续可以结合飞书的多维表格，这样有些场景下的使用就跳出了coze 的web 框架。在要求不严格的场景下，的确可以给个人、员工带来便利。特别是人不想干的脏活，哈哈 交给它做 一点没负担。 ","date":"2025-08-10T00:14:23+08:00","permalink":"https://blog.cba.nxlan.cn/p/coze_01/","title":"扣子上手初体验 - 个人兴趣助手"},{"content":"自我介绍\n一名AI 探路人，相信AI可以给你带来一坛酒——辛辣，但解恨。\nAI 是个筐，我用它装酒？ 记得22年底：ChatGPT 火爆国内外时，觉得有必要玩一下。废了半天劲注册帐号、开通国外信用卡，结果问它工作中的问题：有时很惊艳，有时看它睁眼说瞎话（同事说那会经常听到我对gpt说“are you sure”这句话）。当时用下来，觉得用它生成一些结构化的文档还挺方便的，慢慢地就把它当作一个工作助手。\n一年前，利用AI 文生图、文生视频火起来。群里出现教大家注册相关平台帐号，倒卖帐号，制作创意视频的案例。\n半年多前，AI 编程工具火起来。一小时上手开发个网站、做个游戏。\n今年过年前，DeepSeek 问世，感慨终于有自己的大语言模型了。我自己也折腾了在PC上用游戏显卡本地运行DeepSeek-r1的方案： 体验了带推理能力的大模型和之前的不同。\n最近，MCP RAG Agent 一堆概念又火出圈，逐渐向着更细致的行业和工作场景延伸。\n不难发现AI 圈有两个趋势：\nAI 的这个“大筐”里装的东西越来越多； AI 的发展和应用在向着工作场景加速，不再是刚开始那个“are you sure?” 的助手了。 那下一步，AI 是不是就会取代你我的岗位了？AI的边界在哪里？ 所以，近期打算输出一些 自己在AI道路上的内容。献丑了 ^^||\n今天是第一篇： 整理AI产品，初探内在特质。\n市面上这么多AI 产品，能不能分个类？ 最早，我自己对于 AI分类是非常模糊的，感觉就像老师布置了个作业：要大家“把所有的豆子进行分类”。\n内心OS： 我豆子都认不全，咋分类——要么 按颜色分？要么按味道分？要么按形状分？——圆的一类，扁口的一类。\n但以后的AI产品肯定会诞生 各种形态的：说不准哪天再出来个“三角形”的，到时继续建新的类目？所以，只凭外观性状去分类肯定是不行的，按照术语说这叫没法穷尽——\n这里找AI 介绍下分类原则：\n同一标准原则 分类时必须采用同一标准（维度、角度），不能混用多个标准。 例如：以“颜色”分类水果时，只能按颜色分组，不能同时按大小、产地混合分类。\n相互排斥原则 各类别之间互不重叠，每个事物只能归入一个类别。 例如：以“性别”分类人群，男性和女性互不重叠。\n完全穷尽原则 分类应覆盖所有被分类对象，没有遗漏。 例如：以“季节”分类，一年四季（春夏秋冬）应涵盖全年。\n层次分明原则 分类应有清晰的层次结构，上下级关系明确。 例如：动物→哺乳动物→犬科动物→狗。\n同类相从原则 同一类别中的事物应具有本质相同或相似的属性。 例如：以“使用场合”分类鞋子，运动鞋、皮鞋、拖鞋各归一类。\n单一归属原则 一个对象在同一分类体系下只能归属于一个类别（与相互排斥原则类似）。\n具体明确原则 分类标准和类别定义应具体、明确、易于理解和操作。\n如果从 AI 产品诞生的维度看，不外乎以下四个方面： AI 分析并实现用户意图的能力；用户需要调试AI的深度；用户给AI提供多少已有数据； 用户的预期。不同的AI 产品在这四个维度上，会有侧重的能力和应用场景。\nAI 能力 调试AI复杂度 数据源 用户预期 AI 的优势 产品案例 - - - - 辅助查找问题\u0026amp;信息 AI chat，AI 知识问答 - ⭐ - ⭐ 普通人 也体会到之前有的工作，可能就是一句话的事情 文生图，文生视频，文生广告设计，使用OLLAMA 本地集成大语言模型 ⭐ ⭐ ⭐ ⭐ 进一步考虑把AI 引入到工作中去，去处理那些难度不高 但重复性高的工作 AI 合同评估； AI 文档处理（概括 or 扩容）；AI 邮件发送 ⭐⭐ ⭐ - ⭐ 切入商业环境，做有SOP路径的工作流平台。承担一个完整工作流的工作 企业AI 知识库，AI 客服机器人 ⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐ 进一步进入商业环境，做多个路径的工作流。承担一个岗位面临多个工作流的工作。 AI 医生，AI HR， AI 老师，AI 律师 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐ 供某个行业使用的大模型，往往在通用模型基础上针对性训练。 医疗通用大模型；芯片设计通用模型；教育通用模型 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ 各大公司 研发大模型，希望在自己商业体系中 覆盖行业\u0026amp;客户的需求。 千问 gpt deepseak 豆包 这么多AI 产品有没有递进关系？ 考虑到AI产品和组件会快速发展下去，其中基座肯定是LLM大模型，但是组件之间又是平行的关系——没有必选的组件，而是根据使用场景把它集成进来。所以，我是打算从为用户提供价值的角度来分个层级，层级和层级之间是递进的关系。\n比如：用户是希望整合现成的接口，那AI 就负责上下文记忆、整理和遵循现有工具处理这些数据就好了。 这种应用场景案例有：先需求生文字，文生图片和视频，然后调用工具把文字和视频对齐，就生成一条短视频啦。\n然后，用户觉得只对接现有的接口还不够，希望利用AI的专家推理能力，结合用户自己的知识库，整合成符合特定行业需求的高级助手。这种场景的案例有：cursor claude 等编程工具，用户把自己的需求和项目框架讲清楚，这些工具可以基于已训练出来的代码能力，生成相应功能的代码 交付用户使用。\n还是上面这种场景，用户需要更高的确定性：尽可能降低相同输入却不同输出的不一致性。就需要使用可定制的工作流平台——去明细每一个步骤做什么、输入什么、输出什么。这种场景案例，肯定就是基于coze dify 这种工作流平台诞生的应用了。如：HR 助手，合同助手等针对特定岗位的助手应用。\n最后，少数头部玩家，肯定不满足于少数几个岗位的助手类应用，他们需要的是覆盖行业甚至更通用范式的大模型，例如 跨医疗领域大模型，它可以高效\u0026amp;准确的处理 整个行业中的高价值环节。\n所以，从以上用户需求角度分析的话，分这么四个层级：\n阶段 成本 通用性 确定性 自然语言识别和功能集成能力 ⭐ ⭐⭐⭐ - 特定领域推理能力 ⭐⭐ ⭐ ⭐ 受控制的调试\u0026amp;对接能力 ⭐ ⭐⭐ ⭐⭐ 基于行业的通用识别\u0026amp;推理能力 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ 如果从工作场景的角度出发，我更看好第三层级，因为这里AI的使用在成本和质量方面取得了较好的平衡。\nAI这么厉害，有没有它做不了的？ 它目前还只存在于数字空间，不能送外卖，也不能捏个脚。\n目前只在特定场景下，AI 和人类进行数字交互是没问题的。平时购物时，就可以感受到各大电商平台的客服 回答那些高频问题时是很准的。\n而其他领域，有待AI 进一步升级、进化 。可以想像未来某一天，AI 宠物 可以存储宠物数据平台生成的定制化宠物性格，并实时处理外接的反馈和动作。\n违反社会伦理、道德、法律的事情做不了。例如：下面这个问题——\n还有一种是无法结构化的事情：它能猜你想要的，例如基于人类某时刻的感受和情感做一幅画，但生成的内容质量不可控。\n好啦，先说这么多。下期打算先从coze 入手，展示场景一中个人助手的小案例。\n","date":"2025-08-01T18:54:29+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-1754133256294-8mpclhm3.png","permalink":"https://blog.cba.nxlan.cn/p/askai/","title":"整理AI产品，初探内在特质"},{"content":"前提\u0026amp;背景： 1）家中宽带申请到了公网ipv4，也有自己的域名；\n2）平时在外面（旅游、出差）都需要拨vpn回到家中，这样ddns 就成了刚需；\n3）之前域名管理注册到dnspod CN上，很稳定。只是因为最近把域名管理迁移到了cloudflare中，所以利用现有的iStoreOS（N1）重新关联vpn域名到公网ipv4。\n一开始查了下网上的资料和视频，搞得太复杂。其实两个步骤就可以搞定，所以这里更新下。\n准备 cloudflare dns 令牌 登录自己的cloudflare 帐号 （https://dash.cloudflare.com/login）\n登录成功后选择自己的域名进入\n在域名页面下方，找到“获取API 令牌”的链接\n点击进入该页面，选择第一个模板：创建一个可以编辑dns的令牌。该令牌创建成功后，会用于后续openwrt 中插件更新指定域名。\n点击创建令牌后，会获取到这样一个令牌值。这个令牌 记得复制下来，一会要用！\n同时，可以在 iStoreOS 命令行中使用下面的curl 命令测试该令牌是否生效：\n1 2 curl \u0026#34;https://api.cloudflare.com/client/v4/user/tokens/verify\u0026#34; \\ -H \u0026#34;Authorization: Bearer qx3t1mdQ3hP4oCtR2Yw0usLFev1l1Nh0ipcoMJGA\u0026#34; ​ 这里的信息“Bearer qx3t1mdQ3hP4oCtR2Yw0usLFev1l1Nh0ipcoMJGA” 会在后续 ddns-scripts 中（下面两种方法的第二种）用到。\niStoreOS 中安装ddns插件 经过实测，两个插件都可以实现。不过一个配置一目了然、一个配置项看着复杂一点。\n先说简单的：\nDDNS-go 插件 1）该插件在 iStoreOS中需要 在应用商店安装（下图第二个）：\n2）安装成功后，进入ddns-go 插件配置页。先勾选启用选项，然后保存并应用生效：\n3）服务启动成功后，进入配置页面：\n有常见的 域名服务商。这里选择“Cloudflare”，并在Token中输入上面已经创建好的dns api令牌“qx3t1mdQ3hP4oCtR2Yw0usLFev1l1Nh0ipcoMJGA”\n4）下方 IPv4 配置中，输入这里希望管理的二级域名： vpn.nxlan.cn\n5）因为，我这里不使用ipv6，其他配置不动，点击“save”保存生效。一定要点击保存才会生效。\n​ 生效后，注意观察当前页面右侧的日志：\n1 2 3 4 5 6 7 8 2025/07/22 10:41:45 配置文件已保存在: /etc/ddns-go/ddnsgo-config.yaml 2025/07/22 10:41:45 连接失败! 点击查看接口能否返回IPv4地址 2025/07/22 10:41:45 错误信息: Get \u0026#34;https://myip4.ipip.net\u0026#34;: dial tcp4: lookup myip4.ipip.net on 127.0.0.1:53: no such host 2025/07/22 10:41:46 你的IP 36.161.39.63 没有变化, 域名 vpn.nxlan.cn 2025/07/22 10:41:48 配置文件已保存在: /etc/ddns-go/ddnsgo-config.yaml 2025/07/22 10:41:48 连接失败! 点击查看接口能否返回IPv4地址 2025/07/22 10:41:48 错误信息: Get \u0026#34;https://myip4.ipip.net\u0026#34;: dial tcp4: lookup myip4.ipip.net on 127.0.0.1:53: no such host 2025/07/22 10:41:49 你的IP 36.161.39.63 没有变化, 域名 vpn.nxlan.cn 最后一行显示了，目前我们家中宽带的ip地址注册上了。\n6）最后测试：\n先使用nslookup 验证自己的vpn域名-ip 对应关系是否一样，再使用手机断开wifi 使用4g/5g 移动网络拨VPN 测试。\n1 2 3 4 5 6 7 % nslookup vpn.nxlan.cn 223.5.5.5 Server:\t223.5.5.5 Address:\t223.5.5.5#53 Non-authoritative answer: Name:\tvpn.nxlan.cn Address: 36.161.39.63 然后是第二种插件配置方法：\nddns-scripts 插件 1）该插件默认在 iStorOS 中有安装。同样，先找到这个配置页面：\n2）先启动服务，让ddns 运行：\n3）配置页面下方，点击“添加新服务”。dns服务商 选则cloudflare -v4\n4）先编辑刚刚创建的服务配置：\n5）添加域名信息：\n​ 关键一步：输入用户名、密码。 分别对应刚才生成的DNS Token 中 “ Bearer qx3t1mdQ3hP4oCtR2Yw0usLFev1l1Nh0ipcoMJGA” 这块。\n6）“高级设置”中添加，IP地址来源为URL，检测ip 用的url为 http://checkip.dyndns.com\n​ 其他配置项目，不用改动。最后点击“保存”。\n7） 上面保存好后，返回到服务页面，再点击“保存并应用”就生效了。\n​ 最后不点击这个按钮，插件不会应用新添加的配置。\n8） 检查方法：一种方法是点击编辑中的“日志查看器”：\n还有一种方法，是观察当前服务状态中是否已经出现了对应ip。\n这样，第二种插件使用方法，也就完成啦。\nQ\u0026amp;A 服务成功运行后，关联的是国外ip 怎么办？\n1 2 3 4 5 6 很可能是上述提到的几个url 走到梯子上去了国外。 需要检查用于检测ip的几个域名规则是否走到了国外： - http://checkip.dyndns.com - https://myip4.ipip.net - https://ddns.oray.com/checkip - https://ip.3322.net 域名关联的ip 是国内ip，可是该ip无法从手机上访问到 怎么办？\n1 2 3 首先，排除自己访问的服务不能是 80 443 这种不能发布的服务。 其次，确认下运营商给你的ip 是可以从外部访问的ip，而不是一个公共ip。例如 我这里举例的ip（36.161.39.63）就是一个公共ip。 最后，检查自己光猫上的端口映射。因为，即使外部访问这个ip到了光猫上，光猫没有映射服务到内网主机，仍然是无法从外部访问自己家中服务的。 如果我希望公网地址发生变动时通知我，可以实现么？\n可以在DDNS-go的webhook功能中 追加通知配置——\n这里演示的是飞书 通道。\n具体配置项：\nHeaders 1 Content-Type: application/json RequestBody 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { \u0026#34;msg_type\u0026#34;: \u0026#34;post\u0026#34;, \u0026#34;content\u0026#34;: { \u0026#34;post\u0026#34;: { \u0026#34;zh_cn\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;您的谷歌云主机公网IP变了\u0026#34;, \u0026#34;content\u0026#34;: [ [ { \u0026#34;tag\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;新的IPv4地址：#{ipv4Addr}\u0026#34; } ], [ { \u0026#34;tag\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;域名更新结果：#{ipv4Result}\u0026#34; } ] ] } } } } ","date":"2025-07-22T09:29:49+08:00","image":"https://r2.blog.nxlan.cn/PicGoai-image-1753148120500-bf5gckpy.png","permalink":"https://blog.cba.nxlan.cn/p/ddns/","title":"OpenWRT上ddns 配置案例 [CloudFlare]"},{"content":"测试图片1 测试图片2 ","date":"2025-05-29T17:42:18+08:00","permalink":"https://blog.cba.nxlan.cn/p/11/","title":"测试1"}]