盘点L站的徽章 长期更新!(2026.2.17更新) 文档共建 盘点 L 站的徽章长期更新!(2026.2.17 更新) [!warning] 注意! 做 Wiki 编辑的徽章就做,别把整个教程帖子内容覆盖成无意义的内容!!!!!!!!! … 徽章说明 1 个帖子 - 1 位参与者 阅读完整话题
盘点L站的徽章 长期更新!(2026.2.17更新) 文档共建 盘点 L 站的徽章长期更新!(2026.2.17 更新) [!warning] 注意! 做 Wiki 编辑的徽章就做,别把整个教程帖子内容覆盖成无意义的内容!!!!!!!!! [!caution] 注意 由于服务器有延迟,可能… 我看百倍佬帖子下面全是社区分数不够的开个教程吧,上面这个是社区分数,只有做任务拿勋章还有升三级会给分数,你不做任务永远没分数 credit.linux.do LINUX DO Credit Linux Do 社区积分服务平台 这个ldc,每天登录送10,有人点赞加一但是不能求赞,解决别人问题加10 剧透 ,除此之外在ld士多也能交易 LD士多 LD士多-LinuxDo站点积分兑换中心 在 LD士多 使用 Linux.do 社区积分兑换精选虚拟物品与服务。 1 个帖子 - 1 位参与者 阅读完整话题
[!Warning] 错误概要: Claude cli v2.1.154 国产模型报400 API Error: 400 Failed to deserialize the JSON body into the target type: messages[1].role: unknown variant system , expected user or assistant at line 1 column 994` 明确 messages 数组要求的role 不包含 system 项 通过分析发现 是新 beta 请求头 mid_conversation_system 带来的变化 而三方厂商肯定没有跟进 所以现给出解决方法 以 DeepSeek 为例 ~/.claude/settings.deepseek.json { "env": { "CLAUDE_CODE_USE_FOUNDRY": "1", "ANTHROPIC_FOUNDRY_BASE_URL": "https://api.deepseek.com/anthropic", "ANTHROPIC_FOUNDRY_API_KEY": "sk-", "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]", "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES": "thinking,adaptive_thinking,temperature,effort,max_effort", "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-flash[1m]", "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES": "thinking,adaptive_thinking,temperature,effort,max_effort", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash[1m]", "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES": "thinking,adaptive_thinking,temperature,effort,max_effort" }, "model": "opus", "effortLevel": "max" } claude --settings ~/.claude/settings.deepseek.json 至于 --settings 分配置启动的细节 回顾 Any牌路由器使用清障! ps: 你可能会疑惑 怎么 一会 路由上写 [1m] 一会不写 用 “model”: “opus[1m]” 来代替的 聪明的你应该发现 [1m] 只能有一处有 至于为什么现在的这个 又改到 路由指定上写了呢 是因为 *_SUPPORTED_CAPABILITIES 需要去匹配模型id (opus[1m] 匹配不到) 所以不得已转了个弯 把 [1m] 写路由指代上去 我自己用哪还折腾这么多 直接Patch干掉! 写这个是为了 用 Claude Desktop 的佬~ 这样不用捣鼓他那破bun捆包了 问题 轻松秒杀~ 28 个帖子 - 28 位参与者 阅读完整话题
本期主题:解决ipv6稳定性和DDNS同步问题 前两期回顾: 【超详细】手机搭建服务器 · 第 一 期 【超详细】手机搭建服务器 · 第 二 期 【超详细】手机搭建服务器 · 第 三 期 [!warning]第三期的内容可以不用管 第三期的内容主要是做一个记录,告诉大家在实现手机做服务器这个事情上,有这么一个环节需要去做,如果第三期没有实现成功,不用管,可以直接忽略,然后用本期的代码。因为这一期会有新的代码变更。用本期的代码就好了 前言: [!note]域名服务商推荐 在DDNS同步这个问题上,我推荐大家去华为购买服务器,因为我发现,只有华为的服务器能够做到给免费用户的TTL解析值降低到1s。阿里云是10分钟,腾讯云是5分钟。如果要设置1s,就需要成为付费用户。之前我一直用阿里云的服务器。后来因为这个原因改成华为了。 教程开始 一、如何让ipv6一直保持健 这里先说一下,当你的手机连接wifi以后,不是说你的ipv6就一直可以用了,因为ipv6是有有效期的,一旦过了有效期。ipv6就被废弃了,需要新的ipv6地址。所以ipv6不是大家想的,只要一直连着Wi-Fi,ipv6就是永远有效的。所以第一步,就是如何让手机的ipv6处于一直可用的状态。 1. 打开MT管理器,进入 /data/adb/service.d/ 文件夹,更新 99-phone-server.sh 代码 #!/system/bin/sh # # 99-wifi-keeper.sh # Magisk service.d 启动脚本 # BASE_DIR="/data/local/wifi-keeper" MAIN_SCRIPT="$BASE_DIR/wifi-keeper.sh" LAUNCH_LOG="$BASE_DIR/service.d-launcher.log" mkdir -p "$BASE_DIR" 2>/dev/null echo "============================================================" >> "$LAUNCH_LOG" echo "[$(date '+%Y-%m-%d %H:%M:%S')] service.d launcher started" >> "$LAUNCH_LOG" echo "MAIN_SCRIPT=$MAIN_SCRIPT" >> "$LAUNCH_LOG" if [ ! -f "$MAIN_SCRIPT" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: main script not found" >> "$LAUNCH_LOG" exit 0 fi chmod 755 "$MAIN_SCRIPT" 2>/dev/null ( echo "[$(date '+%Y-%m-%d %H:%M:%S')] launching main script in background" >> "$LAUNCH_LOG" if command -v nohup >/dev/null 2>&1; then nohup sh "$MAIN_SCRIPT" >/dev/null 2>&1 & else sh "$MAIN_SCRIPT" >/dev/null 2>&1 & fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] launcher finished" >> "$LAUNCH_LOG" ) & exit 0 2 进入 /data/local/ 文件夹,新建 wifi-keeper 文件夹,然后进入 wifi-server 文件夹再新建 wifi-keeper.sh 文件;输入以下内容并保存: #!/system/bin/sh # # wifi-keeper.sh # # 作用: # 1. 开机后自动保障 Wi-Fi 连接。 # 2. 使用 cmd wifi connect-network 主动连接指定 Wi-Fi。 # 3. 不使用 wpa_cli / add-suggestion / add-request。 # 4. 获取 2 开头、非 temporary、带 mngtmpaddr 的长期 IPv6。 # 5. 每隔固定时间检查 IPv6 健康度。 # 6. 如果 IPv6 不健康,则重启 Wi-Fi,再重新连接。 # 7. 如果有效 IPv6 相比上一次发生变化,则执行 DDNS 脚本。 # # ============================================================ # 一、基础配置 # ============================================================ # 开机后等待多久再开始执行主逻辑。 # 原因: # Android 刚开机时,Wi-Fi 服务、网络服务、Magisk service.d 可能还没有完全稳定。 # 这里等待 10 秒,可以减少开机初期误判。 BOOT_DELAY_SECONDS=10 # IPv6 健康度检查间隔。 # 每隔多少秒检查一次是否仍然存在有效 IPv6。 # 你当前要求是每 10 秒检查一次。 IPV6_HEALTH_INTERVAL=10 # 打开 Wi-Fi 后,最多等待多少秒确认 Wi-Fi 已经开启。 # 如果超过这个时间仍然检测不到 Wi-Fi 开启,就认为本轮失败。 WAIT_WIFI_ON_SECONDS=40 # 扫描 Wi-Fi 后等待多少秒,让扫描结果刷新。 # Android 的扫描结果有时不是马上返回,适当等待更稳。 WAIT_SCAN_SECONDS=8 # 发起 Wi-Fi 连接后,最多等待多少秒确认已经连接成功。 # 如果第一个 Wi-Fi 在这个时间内没有连接成功,就尝试下一个 Wi-Fi。 WAIT_CONNECT_SECONDS=60 # Wi-Fi 连接成功后,最多等待多少秒获取有效 IPv6。 # 有些路由器下发 IPv6 会比 Wi-Fi 连接慢,所以这里不能太短。 WAIT_IPV6_SECONDS=80 # 各类等待循环中的检查间隔。 # 比如每 2 秒检查一次 Wi-Fi 是否开启、是否连接成功、IPv6 是否出现。 CHECK_INTERVAL=2 # 当 IPv6 不健康时,需要重启 Wi-Fi。 # 关闭 Wi-Fi 后等待多少秒再重新打开。 WIFI_RESTART_OFF_SECONDS=5 # 连接 Wi-Fi 时是否尝试使用固定 MAC,而不是随机 MAC。 # 1 = 优先使用 -r none # 0 = 不使用 -r none # # 对于手机做服务器,建议为 1。 # 好处: # 路由器里看到的 MAC 更稳定; # DHCP 绑定、设备识别、IPv6 分配可能更稳定。 PREFER_MAC_RANDOMIZATION_NONE=1 # 日志保留天数。 # 超过这个天数的动作日志会被自动删除。 LOG_RETENTION_DAYS=7 # 是否在首次获取到有效 IPv6 时执行 DDNS 脚本。 # 1 = 执行 # 0 = 只记录,不执行 # # 建议为 1。 # 因为手机重启后,即使 IPv6 和上次一样,也可能需要重新同步一次 DDNS。 DDNS_ON_FIRST_IP=1 # DDNS 脚本路径。 # 当检测到有效 IPv6 发生变化时,会执行这个脚本。 # # 执行方式: # sh /data/local/ipv6-ddns/ipv6-ddns.sh 当前IPv6地址 # # 同时也会传递环境变量: # CURRENT_IPV6 # VALID_IPV6 # WIFI_SSID DDNS_SCRIPT="/data/local/ipv6-ddns/ipv6-ddns.sh" # IPv6 历史记录最多保留多少行。 # 这个不是主日志,只是专门记录每次有效 IPv6 变化。 IPV6_HISTORY_MAX_LINES=500 # ============================================================ # 二、Wi-Fi 列表 # ============================================================ # # 格式: # WiFi名称|WiFi密码|加密方式 # # 加密方式: # WPA2 写 WPA2 即可,脚本内部会自动转成 cmd wifi 需要的 wpa2。 # 如果是开放网络,可以写 open。 # # 说明: # 1. 每一行代表一个 Wi-Fi。 # 2. 按照从上到下的顺序优先连接。 # 3. 第一个连接失败,才会尝试下一个。 # 4. 不管系统是否保存过 Wi-Fi,都会用这里写死的名称和密码主动连接。 # WIFI_LIST=' CMCC-Semmering|Semmering|WPA2 ' # ============================================================ # 三、路径配置 # ============================================================ SELF="$0" case "$SELF" in /*) SELF_PATH="$SELF" ;; *) SELF_PATH="$(pwd)/$SELF" ;; esac SCRIPT_DIR="$(cd "$(dirname "$SELF_PATH")" 2>/dev/null && pwd)" [ -n "$SCRIPT_DIR" ] || SCRIPT_DIR="$(pwd)" # 主日志。 # 最新动作在最上面。 LOG_FILE="$SCRIPT_DIR/wifi-keeper.log" # 临时目录。 # 用于临时存放单个动作日志,动作结束后再整体插入到 LOG_FILE 顶部。 # 这个目录不是业务数据,脚本停止后可以删除。 TEMP_DIR="$SCRIPT_DIR/wifi-keeper-temp" # 保存有效 IPv6 记录。 # 格式: # YYYY-MM-DD HH:mm:ss|IPv6地址|来源动作|previous=上一次IPv6 # # 说明: # 1. 只保留这一个 IPv6 记录文件,不再单独保存 last-valid-ipv6.txt。 # 2. 脚本会从这个文件尾部读取最新 IPv6,用于和本次 IPv6 做 diff。 # 3. 只有首次记录或者 IPv6 发生变化时,才会追加新记录。 IPV6_HISTORY_FILE="$SCRIPT_DIR/valid-ipv6-history.txt" mkdir -p "$TEMP_DIR" 2>/dev/null touch "$LOG_FILE" "$IPV6_HISTORY_FILE" 2>/dev/null if [ ! -w "$LOG_FILE" ]; then echo "ERROR: 无法写入日志文件:$LOG_FILE" echo "建议把脚本放到可写目录,例如:/data/local/phone-server/wifi-keeper.sh" exit 1 fi # ============================================================ # 四、全局变量 # ============================================================ WIFI_IFACE="wlan0" ACTION_FILE="" ACTION_NAME="" ACTION_SEQ=0 LAST_CONNECTED_SSID="" LAST_VALID_IPV6="" HEALTH_FAIL_REASON="" # ============================================================ # 五、基础工具函数 # ============================================================ now_ts() { date '+%Y-%m-%d %H:%M:%S' 2>/dev/null } now_epoch() { date '+%s' 2>/dev/null } print_line() { printf '%s\n' "$*" } get_wifi_iface() { IFACE="$(ip link 2>/dev/null \ | sed -n 's/^[0-9][0-9]*: \(wlan[0-9][^: ]*\).*/\1/p' \ | head -n 1 \ | sed 's/@.*//')" [ -n "$IFACE" ] || IFACE="wlan0" print_line "$IFACE" } normalize_security() { SEC="$(print_line "$1" | tr 'A-Z' 'a-z')" case "$SEC" in wap2|wpa-psk|psk) SEC="wpa2" ;; wpa2|wpa3|open|owe) ;; *) SEC="wpa2" ;; esac print_line "$SEC" } # ============================================================ # 六、日志函数 # ============================================================ start_action() { ACTION_NAME="$1" ACTION_SEQ=$((ACTION_SEQ + 1)) TS="$(now_ts)" EPOCH="$(now_epoch)" [ -n "$EPOCH" ] || EPOCH="0" ACTION_FILE="$TEMP_DIR/action.$$.${ACTION_SEQ}.log" { print_line "##### ACTION_BLOCK_BEGIN #####" print_line "============================================================" print_line "===== $TS | $ACTION_NAME | START =====" print_line "============================================================" print_line "LOG_EPOCH: $EPOCH" } > "$ACTION_FILE" } log_detail() { TS="$(now_ts)" if [ -n "$ACTION_FILE" ]; then print_line "[$TS] $*" >> "$ACTION_FILE" else print_line "[$TS] $*" >> "$LOG_FILE" fi } log_multiline() { PREFIX="$1" CONTENT="$2" if [ -n "$CONTENT" ]; then print_line "$CONTENT" | while IFS= read -r LINE || [ -n "$LINE" ]; do log_detail "$PREFIX$LINE" done else log_detail "${PREFIX}<empty>" fi } prune_log_by_days() { [ "$LOG_RETENTION_DAYS" -gt 0 ] 2>/dev/null || return 0 NOW="$(now_epoch)" [ -n "$NOW" ] || return 0 CUTOFF=$((NOW - LOG_RETENTION_DAYS * 86400)) PRUNED="$TEMP_DIR/pruned.$$.log" awk -v cutoff="$CUTOFF" ' BEGIN { inside = 0 block = "" keep = 1 } $0 == "##### ACTION_BLOCK_BEGIN #####" { if (inside == 1 && keep == 1) { printf "%s", block } inside = 1 block = $0 "\n" keep = 1 next } inside == 1 { block = block $0 "\n" if ($1 == "LOG_EPOCH:") { if (($2 + 0) < cutoff) { keep = 0 } } if ($0 == "##### ACTION_BLOCK_END #####") { if (keep == 1) { printf "%s", block } inside = 0 block = "" keep = 1 } next } { print } END { if (inside == 1 && keep == 1) { printf "%s", block } } ' "$LOG_FILE" > "$PRUNED" 2>/dev/null if [ -s "$PRUNED" ] || [ -f "$PRUNED" ]; then mv "$PRUNED" "$LOG_FILE" 2>/dev/null else rm -f "$PRUNED" 2>/dev/null fi } end_action() { RESULT="$1" TS="$(now_ts)" { print_line "[$TS] RESULT: $RESULT" print_line "============================================================" print_line "===== $TS | $ACTION_NAME | END: $RESULT =====" print_line "============================================================" print_line "##### ACTION_BLOCK_END #####" print_line "" } >> "$ACTION_FILE" MERGED="$TEMP_DIR/merged.$$.log" cat "$ACTION_FILE" "$LOG_FILE" > "$MERGED" 2>/dev/null && mv "$MERGED" "$LOG_FILE" rm -f "$ACTION_FILE" 2>/dev/null ACTION_FILE="" ACTION_NAME="" prune_log_by_days } run_cmd_args() { SHOW="$1" shift log_detail "CMD : $SHOW" OUT="$("$@" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" return "$RC" } # ============================================================ # 七、Wi-Fi 状态判断 # ============================================================ is_wifi_enabled() { STATUS="$(cmd wifi status 2>/dev/null)" print_line "$STATUS" | grep -qi "Wifi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wi-Fi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wifi is enabled" && return 0 print_line "$STATUS" | grep -qi "Wi-Fi is enabled" && return 0 print_line "$STATUS" | grep -qi "Wifi is connected" && return 0 print_line "$STATUS" | grep -qi "Wi-Fi is connected" && return 0 return 1 } is_connected_to_ssid() { TARGET_SSID="$1" STATUS="$(cmd wifi status 2>/dev/null)" print_line "$STATUS" | grep -qi "Wifi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wi-Fi is disabled" && return 1 if print_line "$STATUS" | grep -qi "Wifi is connected" \ && print_line "$STATUS" | grep -F "\"$TARGET_SSID\"" >/dev/null 2>&1; then return 0 fi if print_line "$STATUS" | grep -qi "Wi-Fi is connected" \ && print_line "$STATUS" | grep -F "\"$TARGET_SSID\"" >/dev/null 2>&1; then return 0 fi if command -v iw >/dev/null 2>&1; then IW_STATUS="$(iw dev "$WIFI_IFACE" link 2>/dev/null)" print_line "$IW_STATUS" | grep -q "^Connected to " || return 1 print_line "$IW_STATUS" | grep -F "SSID: $TARGET_SSID" >/dev/null 2>&1 && return 0 fi return 1 } get_connected_ssid() { STATUS="$(cmd wifi status 2>/dev/null)" SSID_NOW="$(print_line "$STATUS" | sed -n 's/.*connected to "\([^"]*\)".*/\1/p' | head -n 1)" if [ -n "$SSID_NOW" ]; then print_line "$SSID_NOW" return 0 fi if command -v iw >/dev/null 2>&1; then iw dev "$WIFI_IFACE" link 2>/dev/null | sed -n 's/^[[:space:]]*SSID: //p' | head -n 1 return 0 fi print_line "" } is_connected_to_any_configured_wifi() { LIST_FILE="$TEMP_DIR/wifi-list-any.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac if is_connected_to_ssid "$SSID"; then rm -f "$LIST_FILE" 2>/dev/null return 0 fi done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null return 1 } # ============================================================ # 八、Wi-Fi 操作函数 # ============================================================ log_status() { log_detail "---------- 当前 Wi-Fi / IP 状态 ----------" run_cmd_args "cmd wifi status" cmd wifi status if command -v iw >/dev/null 2>&1; then run_cmd_args "iw dev $WIFI_IFACE link" iw dev "$WIFI_IFACE" link else log_detail "INFO : 当前系统没有 iw 命令,跳过 iw dev link" fi run_cmd_args "ip addr show dev $WIFI_IFACE" ip addr show dev "$WIFI_IFACE" run_cmd_args "ip -6 addr show dev $WIFI_IFACE" ip -6 addr show dev "$WIFI_IFACE" log_detail "---------- 当前状态结束 ----------" } wait_wifi_enabled() { log_detail "INFO : 等待 Wi-Fi 开启,最多 ${WAIT_WIFI_ON_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_WIFI_ON_SECONDS" ]; do if is_wifi_enabled; then log_detail "OK : Wi-Fi 已开启" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : Wi-Fi 尚未确认开启,已等待 ${ELAPSED}s" done log_detail "ERROR: Wi-Fi 开启超时" return 1 } ensure_wifi_on() { if is_wifi_enabled; then log_detail "OK : Wi-Fi 当前已经开启,不重复打开" return 0 fi log_detail "INFO : Wi-Fi 当前关闭,开始打开" if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi enable" svc wifi enable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi enable" fi run_cmd_args "cmd wifi set-wifi-enabled enabled" cmd wifi set-wifi-enabled enabled wait_wifi_enabled } scan_wifi() { log_detail "INFO : 开始扫描周围 Wi-Fi" run_cmd_args "cmd wifi start-scan" cmd wifi start-scan log_detail "INFO : 等待扫描结果刷新 ${WAIT_SCAN_SECONDS}s" sleep "$WAIT_SCAN_SECONDS" SCAN_RESULT="$(cmd wifi list-scan-results 2>&1)" log_detail "CMD : cmd wifi list-scan-results" log_multiline "SCAN : " "$SCAN_RESULT" LIST_FILE="$TEMP_DIR/wifi-list-scan.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac if print_line "$SCAN_RESULT" | grep -F "$SSID" >/dev/null 2>&1; then log_detail "OK : 扫描结果中发现配置 Wi-Fi:$SSID" else log_detail "WARN : 扫描结果中没有发现配置 Wi-Fi:$SSID;仍会继续尝试连接" fi done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null } wait_connected_to_ssid() { TARGET_SSID="$1" log_detail "INFO : 等待连接到 Wi-Fi:$TARGET_SSID,最多 ${WAIT_CONNECT_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_CONNECT_SECONDS" ]; do if is_connected_to_ssid "$TARGET_SSID"; then LAST_CONNECTED_SSID="$TARGET_SSID" log_detail "OK : 已连接到目标 Wi-Fi:$TARGET_SSID" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : 尚未连接到 $TARGET_SSID,已等待 ${ELAPSED}s" done log_detail "ERROR: 连接 $TARGET_SSID 超时" return 1 } connect_one_wifi() { TARGET_SSID="$1" TARGET_PASS="$2" TARGET_SEC="$(normalize_security "$3")" log_detail "------------------------------------------------------------" log_detail "INFO : 尝试连接 Wi-Fi" log_detail "SSID : $TARGET_SSID" log_detail "SEC : $TARGET_SEC" log_detail "NOTE : 不管系统是否保存过该 Wi-Fi,都会使用 SSID + 密码主动连接" if [ "$TARGET_SEC" = "open" ] || [ "$TARGET_SEC" = "owe" ]; then if [ "$PREFER_MAC_RANDOMIZATION_NONE" = "1" ]; then log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC -r none" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" -r none 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" if [ "$RC" != "0" ]; then log_detail "WARN : 带 -r none 的 open/owe 连接失败,回退到不带 -r" run_cmd_args "cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC" \ cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" fi else run_cmd_args "cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC" \ cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" fi else if [ "$PREFER_MAC_RANDOMIZATION_NONE" = "1" ]; then log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\" -r none" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" -r none 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" if [ "$RC" != "0" ]; then log_detail "WARN : 带 -r none 的连接失败,回退到不带 -r" log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\"" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" fi else log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\"" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" fi fi wait_connected_to_ssid "$TARGET_SSID" } connect_by_wifi_list() { LIST_FILE="$TEMP_DIR/wifi-list-connect.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" INDEX=0 while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac INDEX=$((INDEX + 1)) log_detail "INFO : 开始尝试第 ${INDEX} 个 Wi-Fi:$SSID" if connect_one_wifi "$SSID" "$PASS" "$SEC"; then log_detail "OK : 第 ${INDEX} 个 Wi-Fi 连接成功:$SSID" rm -f "$LIST_FILE" 2>/dev/null return 0 fi log_detail "WARN : 第 ${INDEX} 个 Wi-Fi 连接失败:$SSID,准备尝试下一个" done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null log_detail "ERROR: Wi-Fi 列表全部尝试失败" return 1 } # ============================================================ # 九、IPv6 相关函数 # ============================================================ get_valid_ipv6() { ip -6 addr show dev "$WIFI_IFACE" scope global 2>/dev/null | while IFS= read -r ADDR_LINE || [ -n "$ADDR_LINE" ]; do # 只处理 inet6 地址行 set -- $ADDR_LINE [ "$1" = "inet6" ] || continue ADDR_CIDR="$2" IPV6_ADDR="${ADDR_CIDR%%/*}" # 读取下一行 lifetime 信息 IFS= read -r LFT_LINE || LFT_LINE="" # 规则 1:必须是 2 开头 case "$IPV6_ADDR" in 2*) ;; *) continue ;; esac # 规则 2:必须是 mngtmpaddr echo "$ADDR_LINE" | grep -q "mngtmpaddr" || continue # 规则 3:不能是 temporary echo "$ADDR_LINE" | grep -q "temporary" && continue # 规则 4:preferred_lft 不能是 0sec echo "$LFT_LINE" | grep -q "preferred_lft 0sec" && continue echo "$IPV6_ADDR" exit 0 done | head -n 1 } log_ipv6_detail() { IPV6_ALL="$(ip -6 addr show dev "$WIFI_IFACE" 2>&1)" IPV6_GLOBAL="$(ip -6 addr show dev "$WIFI_IFACE" scope global 2>&1)" VALID_IPV6="$(get_valid_ipv6)" log_detail "---------- IPv6 地址详情 ----------" log_multiline "IPv6-ALL : " "$IPV6_ALL" log_multiline "IPv6-GLOBAL : " "$IPV6_GLOBAL" if [ -n "$VALID_IPV6" ]; then log_detail "IPv6-VALID : $VALID_IPV6" log_detail "IPv6-RULE : 命中规则:2 开头 + 非 temporary + mngtmpaddr + preferred_lft 非 0sec" else log_detail "IPv6-VALID : <none>" log_detail "IPv6-RULE : 未找到 2 开头 + 非 temporary + mngtmpaddr 的长期 IPv6" print_line "$IPV6_GLOBAL" | grep -q "temporary" && \ log_detail "IPv6-INFO : 存在 temporary IPv6,但不作为服务器长期地址" print_line "$IPV6_GLOBAL" | grep -E "inet6 f" >/dev/null 2>&1 && \ log_detail "IPv6-INFO : 存在 f 开头 IPv6,不符合要求" print_line "$IPV6_GLOBAL" | grep -q "inet6" || \ log_detail "IPv6-INFO : 当前没有 scope global IPv6" fi log_detail "---------- IPv6 地址详情结束 ----------" } wait_valid_ipv6() { log_detail "INFO : 等待获取有效 IPv6,最多 ${WAIT_IPV6_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_IPV6_SECONDS" ]; do VALID_IPV6="$(get_valid_ipv6)" if [ -n "$VALID_IPV6" ]; then LAST_VALID_IPV6="$VALID_IPV6" log_detail "OK : 已获取有效 IPv6:$VALID_IPV6" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : 尚未获取有效 IPv6,已等待 ${ELAPSED}s" done log_detail "ERROR: 获取有效 IPv6 超时" return 1 } trim_ipv6_history() { [ -f "$IPV6_HISTORY_FILE" ] || return 0 TMP_HISTORY="$TEMP_DIR/ipv6-history.$$.tmp" tail -n "$IPV6_HISTORY_MAX_LINES" "$IPV6_HISTORY_FILE" > "$TMP_HISTORY" 2>/dev/null \ && mv "$TMP_HISTORY" "$IPV6_HISTORY_FILE" 2>/dev/null rm -f "$TMP_HISTORY" 2>/dev/null } ipv6_change_action() { CURRENT_IPV6="$1" SOURCE_ACTION="$2" [ -n "$CURRENT_IPV6" ] || return 0 PREVIOUS_LINE="" PREVIOUS_IPV6="" # 只从 valid-ipv6-history.txt 的最后一行读取上一次 IPv6, # 不再使用独立的 last-valid-ipv6.txt 文件。 if [ -f "$IPV6_HISTORY_FILE" ]; then PREVIOUS_LINE="$(tail -n 1 "$IPV6_HISTORY_FILE" 2>/dev/null)" PREVIOUS_IPV6="$(print_line "$PREVIOUS_LINE" | awk -F'|' '{print $2}')" fi start_action "IPv6地址变更检查" TS="$(now_ts)" CONNECTED_SSID="$(get_connected_ssid)" log_detail "来源动作: $SOURCE_ACTION" log_detail "当前连接 Wi-Fi: ${CONNECTED_SSID:-<none>}" log_detail "有效 IPv6 记录文件: $IPV6_HISTORY_FILE" log_detail "上一条记录: ${PREVIOUS_LINE:-<none>}" log_detail "上一次 IPv6: ${PREVIOUS_IPV6:-<none>}" log_detail "本次 IPv6: $CURRENT_IPV6" if [ -n "$PREVIOUS_IPV6" ] && [ "$CURRENT_IPV6" = "$PREVIOUS_IPV6" ]; then log_detail "DIFF : IPv6 未变化:$CURRENT_IPV6" log_detail "SAVE : IPv6 未变化,不追加有效 IPv6 记录文件" log_detail "DDNS : IPv6 未变化,不执行 DDNS 脚本" end_action "unchanged" return 0 fi if [ -z "$PREVIOUS_IPV6" ]; then log_detail "DIFF : 这是首次记录有效 IPv6" if [ "$DDNS_ON_FIRST_IP" = "1" ]; then SHOULD_RUN_DDNS=1 log_detail "DDNS : DDNS_ON_FIRST_IP=1,首次获取 IPv6 也会执行 DDNS 脚本" else SHOULD_RUN_DDNS=0 log_detail "DDNS : DDNS_ON_FIRST_IP=0,首次获取 IPv6 只记录,不执行 DDNS 脚本" fi else SHOULD_RUN_DDNS=1 log_detail "DIFF : IPv6 已变化:$PREVIOUS_IPV6 -> $CURRENT_IPV6" fi if [ "$SHOULD_RUN_DDNS" != "1" ]; then print_line "$TS|$CURRENT_IPV6|$SOURCE_ACTION|previous=${PREVIOUS_IPV6:-none}|ddns=not_required" >> "$IPV6_HISTORY_FILE" 2>/dev/null log_detail "SAVE : DDNS 不需要执行,已追加有效 IPv6 记录文件:$IPV6_HISTORY_FILE" trim_ipv6_history end_action "done_without_ddns" return 0 fi if [ ! -f "$DDNS_SCRIPT" ]; then log_detail "DDNS : DDNS 脚本不存在,暂不执行:$DDNS_SCRIPT" log_detail "DDNS : 本次 IPv6 不会写入有效 IPv6 记录文件,等待下次健康检查继续重试" end_action "ddns_script_missing" return 1 fi log_detail "DDNS : 准备执行 DDNS 脚本" log_detail "CMD : CURRENT_IPV6=\"$CURRENT_IPV6\" VALID_IPV6=\"$CURRENT_IPV6\" WIFI_SSID=\"$CONNECTED_SSID\" sh \"$DDNS_SCRIPT\" \"$CURRENT_IPV6\"" DDNS_OUT="$( CURRENT_IPV6="$CURRENT_IPV6" \ VALID_IPV6="$CURRENT_IPV6" \ WIFI_SSID="$CONNECTED_SSID" \ sh "$DDNS_SCRIPT" "$CURRENT_IPV6" 2>&1 )" DDNS_RC="$?" log_multiline "DDNS-OUT : " "$DDNS_OUT" log_detail "DDNS-RC : $DDNS_RC" if [ "$DDNS_RC" = "0" ]; then log_detail "DDNS : DDNS 脚本执行成功" print_line "$TS|$CURRENT_IPV6|$SOURCE_ACTION|previous=${PREVIOUS_IPV6:-none}|ddns=success" >> "$IPV6_HISTORY_FILE" 2>/dev/null log_detail "SAVE : DDNS 成功后,已追加有效 IPv6 记录文件:$IPV6_HISTORY_FILE" trim_ipv6_history end_action "done" return 0 fi log_detail "DDNS : DDNS 脚本执行失败" log_detail "SAVE : DDNS 失败,本次 IPv6 不写入有效 IPv6 记录文件,下一轮会继续重试" end_action "ddns_failed" return 1 } # ============================================================ # 十、IPv6 健康检查 # ============================================================ check_ipv6_health() { HEALTH_FAIL_REASON="" CURRENT_SSID="$(get_connected_ssid)" log_detail "INFO : 当前连接 SSID:${CURRENT_SSID:-<none>}" if ! is_connected_to_any_configured_wifi; then HEALTH_FAIL_REASON="当前没有连接到脚本配置的 Wi-Fi" log_detail "ERROR: $HEALTH_FAIL_REASON" log_status log_ipv6_detail return 1 fi log_status log_ipv6_detail VALID_IPV6="$(get_valid_ipv6)" if [ -n "$VALID_IPV6" ]; then LAST_VALID_IPV6="$VALID_IPV6" log_detail "OK : IPv6 健康,有效 IPv6:$VALID_IPV6" return 0 fi HEALTH_FAIL_REASON="没有找到 2 开头、非 temporary、mngtmpaddr 的长期 IPv6 地址" log_detail "ERROR: IPv6 不健康:$HEALTH_FAIL_REASON" return 1 } # ============================================================ # 十一、恢复动作 # ============================================================ restart_wifi_switch() { start_action "Wi-Fi重启恢复" log_detail "INFO : 开始重启 Wi-Fi 开关" log_status if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi disable" svc wifi disable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi disable" fi run_cmd_args "cmd wifi set-wifi-enabled disabled" cmd wifi set-wifi-enabled disabled log_detail "INFO : Wi-Fi 关闭命令已发送,等待 ${WIFI_RESTART_OFF_SECONDS}s" sleep "$WIFI_RESTART_OFF_SECONDS" if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi enable" svc wifi enable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi enable" fi run_cmd_args "cmd wifi set-wifi-enabled enabled" cmd wifi set-wifi-enabled enabled if wait_wifi_enabled; then log_status end_action "success" return 0 fi log_status end_action "failed_wifi_not_enabled" return 1 } ensure_connection_and_ipv6() { REASON="$1" start_action "Wi-Fi连接保障" log_detail "触发原因: $REASON" log_detail "脚本目录: $SCRIPT_DIR" log_detail "主日志: $LOG_FILE" log_detail "临时目录: $TEMP_DIR" log_detail "有效 IPv6 记录文件: $IPV6_HISTORY_FILE" log_detail "Wi-Fi 网卡: $WIFI_IFACE" log_detail "Wi-Fi 列表:" log_multiline " - " "$WIFI_LIST" log_status if ! ensure_wifi_on; then log_status end_action "failed_wifi_not_enabled" return 1 fi scan_wifi if ! connect_by_wifi_list; then log_status end_action "failed_wifi_connect" return 1 fi log_status log_ipv6_detail if wait_valid_ipv6; then log_ipv6_detail CURRENT_VALID_IPV6="$LAST_VALID_IPV6" end_action "success" ipv6_change_action "$CURRENT_VALID_IPV6" "$REASON" return 0 fi log_ipv6_detail end_action "failed_ipv6_not_ready" return 1 } # ============================================================ # 十二、主流程 # ============================================================ # 清理上一次异常退出残留的临时文件。 rm -f "$TEMP_DIR"/action.*.log "$TEMP_DIR"/merged.*.log "$TEMP_DIR"/pruned.*.log 2>/dev/null WIFI_IFACE="$(get_wifi_iface)" start_action "脚本启动" log_detail "START: wifi-keeper" log_detail "PID : $$" log_detail "DIR : $SCRIPT_DIR" log_detail "LOG : $LOG_FILE" log_detail "IFACE: $WIFI_IFACE" log_detail "BOOT_DELAY_SECONDS: $BOOT_DELAY_SECONDS" log_detail "IPV6_HEALTH_INTERVAL: $IPV6_HEALTH_INTERVAL" log_detail "LOG_RETENTION_DAYS: $LOG_RETENTION_DAYS" if ! command -v cmd >/dev/null 2>&1; then log_detail "ERROR: 当前系统没有 cmd 命令,无法使用 cmd wifi" end_action "fatal_no_cmd" exit 1 fi run_cmd_args "id" id run_cmd_args "getprop ro.build.version.release" getprop ro.build.version.release run_cmd_args "getprop ro.build.version.sdk" getprop ro.build.version.sdk run_cmd_args "getprop ro.product.model" getprop ro.product.model run_cmd_args "getprop ro.product.device" getprop ro.product.device run_cmd_args "cmd wifi status" cmd wifi status log_detail "INFO : 开机后先等待 ${BOOT_DELAY_SECONDS}s" sleep "$BOOT_DELAY_SECONDS" log_detail "INFO : 开机等待结束,进入主流程" end_action "success" ensure_connection_and_ipv6 "开机初始化" while true; do sleep "$IPV6_HEALTH_INTERVAL" start_action "IPv6健康度检查" if check_ipv6_health; then CURRENT_VALID_IPV6="$LAST_VALID_IPV6" end_action "healthy" ipv6_change_action "$CURRENT_VALID_IPV6" "IPv6健康度检查" continue fi log_detail "处理策略: IPv6 不健康,准备重启 Wi-Fi 并重新连接" log_detail "失败原因: ${HEALTH_FAIL_REASON:-unknown}" end_action "unhealthy_need_recovery" restart_wifi_switch ensure_connection_and_ipv6 "IPv6健康度异常后的恢复连接" done [!note]重点说下wifi-keeper.sh 这一次wifi-keeper.sh相比较第三期,做出了极大的改变,可以说废弃了第三期的代码。因为我发现了一个重大的问题:在国产的系统中,控制wifi的命令,比如cmd wifi是无法使用的,根本不支持。只有类原生才支持。如果不支持cmd命令就无法做到这些事情: · 无法扫描wifi列表; · 无法查看Wi-Fi状态; · 无法通过wifi名称和Wi-Fi密码连接Wi-Fi(这个才是最致命的); 所以我把手里的MIUI 11刷成了 crDroid 。这里不推荐刷入 Pixel Experience ,因为 Pixel Experience 对cmd wifi的支持也有问题,当然,也可能是小米8SE的包原因,大家自行测试吧。 在wifi-keeper.sh这个脚本中, 大家要修改的地方是WIFI_LIST部分 ,这里需要把你家里的wifi名称和Wi-Fi密码和加密方式写到脚本中,这样不管是你手机重新开关机还是取消保存了wifi,还是开启了飞行模式,脚本都可以很好的自动连接上Wi-Fi。Wi-Fi是不会再断的,哪怕你手动断开wifi,wifi过10s都会自动连接上的。 大家不用担心脚本安全问题,因为脚本是公开透明的。 3.记得修改wifi-keeper.sh文件的权限,可以参考 上一期 如何修改权限,这里就不截图了 二、如何实现ipv6自动更新到DDNS,实现用域名访问 1. 自行去华为云购买服务器 。这里只拿华为云举例,原因开头已经说了。大家自行注册和购买域名。注册好域名后看下一步。 2. 配置AK和SK,后面脚本会用到 2.1 访问 统一身份认证服务IAM ,然后左边菜单找到 用户 ,在 用户 页面 右上角点击创建用户 2.2 如图操作,然后下一步 2.3 到这一步的时候,一定要点击确定下载密钥文件,因为密钥只能在这里下载一次,如果不小心点了取消,只能重新创建用户 3.进入 /data/local/ 文件夹,新建 ipv6-ddns 文件夹,然后进入 ipv6-ddns 文件夹再新建 ipv6-ddns.sh 文件;输入以下内容并保存: [!warning]提醒 记得务必把代码中的AK和SK替换成自己的。 文件同样记得修改权限。 图中域名填写时,小数点不能去掉 图中域名填写时,小数点不能去掉 图中域名填写时,小数点不能去掉 #!/system/bin/sh # # ipv6-ddns.sh # # 作用: # 接收 wifi-keeper.sh 传入的有效 IPv6,然后同步到华为云 DNS AAAA 记录。 # # 核心逻辑: # 1. 接收第一个参数作为新的 IPv6。 # 2. 查询华为云 DNS,获取 ZONE_ID。 # 3. 查询目标 AAAA 记录,读取华为云当前 IPv6。 # 4. 如果华为云当前 IPv6 与传入 IPv6 一致,则不更新。 # 5. 如果不一致,则 PUT 更新记录。 # 6. 每次执行作为一个动作写入日志,最新动作在日志最上方,动作内部细节正序。 # # 注意: # 这个脚本是给 wifi-keeper.sh 调用的: # sh /data/local/ipv6-ddns/ipv6-ddns.sh 2409:xxxx:xxxx::xxxx # # 因为 wifi-keeper.sh 使用 sh 调用 DDNS 脚本,所以本脚本尽量使用 /system/bin/sh 兼容写法, # 不使用 bash 数组。 # # ============================================================ # 一、Termux 工具路径 # ============================================================ # Termux 的安装目录。 # 因为 Magisk service.d 启动的脚本不一定有 Termux 的 PATH, # 所以这里主动把 Termux bin 目录加进去。 PREFIX="/data/data/com.termux/files/usr" export PATH="$PREFIX/bin:/system/bin:/system/xbin:/vendor/bin:$PATH" export LD_LIBRARY_PATH="$PREFIX/lib:${LD_LIBRARY_PATH:-}" # 华为云签名需要 openssl 和 xxd。 # HTTP 请求需要 curl。 OPENSSL_BIN="$PREFIX/bin/openssl" XXD_BIN="$PREFIX/bin/xxd" CURL_BIN="$PREFIX/bin/curl" # 常用命令。 DATE_BIN="/system/bin/date" [ -x "$PREFIX/bin/date" ] && DATE_BIN="$PREFIX/bin/date" MKDIR_BIN="/system/bin/mkdir" [ -x "$PREFIX/bin/mkdir" ] && MKDIR_BIN="$PREFIX/bin/mkdir" CAT_BIN="/system/bin/cat" [ -x "$PREFIX/bin/cat" ] && CAT_BIN="$PREFIX/bin/cat" MV_BIN="/system/bin/mv" [ -x "$PREFIX/bin/mv" ] && MV_BIN="$PREFIX/bin/mv" RM_BIN="/system/bin/rm" [ -x "$PREFIX/bin/rm" ] && RM_BIN="$PREFIX/bin/rm" AWK_BIN="/system/bin/awk" [ -x "$PREFIX/bin/awk" ] && AWK_BIN="$PREFIX/bin/awk" SED_BIN="/system/bin/sed" [ -x "$PREFIX/bin/sed" ] && SED_BIN="$PREFIX/bin/sed" GREP_BIN="/system/bin/grep" [ -x "$PREFIX/bin/grep" ] && GREP_BIN="$PREFIX/bin/grep" TAIL_BIN="/system/bin/tail" [ -x "$PREFIX/bin/tail" ] && TAIL_BIN="$PREFIX/bin/tail" # ============================================================ # 二、用户配置区 # ============================================================ # 华为云访问密钥。 # 为避免把你的 AK/SK 在聊天记录里再次明文展示, # 请把你参考脚本里的 AK 和 SK 填到下面两行。 # # 也可以通过环境变量传入: # HUAWEICLOUD_AK="你的AK" HUAWEICLOUD_SK="你的SK" sh ipv6-ddns.sh 2409:... AK="${填入AK}" SK="${填入SK}" # 华为云 DNS API Host。 HOST="dns.myhuaweicloud.com" # 主域名 Zone 名称。 # 注意:华为云 DNS API 返回的 zone name 通常带结尾点。 ZONE_NAME="自己的域名." # 后面的点不能去掉 # 需要同步的 AAAA 记录列表。 # 一行一个,必须带结尾点。 # 示例: # xxxx.top. # www.xxxx.top. # ssh.xxxx.top. RECORD_NAMES=' xxxx.top. ' # 记录类型,IPv6 固定使用 AAAA。 RECORD_TYPE="AAAA" # DNS TTL。 # 这里沿用你参考脚本里的 TTL=1。 TTL="1" # 调试开关: # 0 = 常规日志 # 1 = 写入更多签名和响应调试信息 # 注意:即使 DEBUG=1,也不会打印 SK。 DEBUG="0" # 日志保留天数。 # 超过这个天数的动作日志会被清理。 LOG_RETENTION_DAYS=7 # HTTP 请求最大重试次数。 # 主要用于处理刚开机、Wi-Fi 刚恢复、IPv6 刚出现时,网络栈还不稳定导致的 curl 56、HTTP 000、5xx 等临时错误。 HTTP_MAX_RETRIES=3 # HTTP 重试基础等待秒数。 # 第 1 次失败后等待 3 秒,第 2 次失败后等待 6 秒。 HTTP_RETRY_BASE_SLEEP_SECONDS=3 # ============================================================ # 三、路径配置 # ============================================================ BASE_DIR="/data/local/ipv6-ddns" LOG_FILE="$BASE_DIR/ipv6-ddns.log" RUNTIME_DIR="$BASE_DIR/.runtime" RUN_LOG="$RUNTIME_DIR/ipv6-ddns.run.$$.log" # ============================================================ # 四、运行参数 # ============================================================ NEW_IPV6="$1" # ============================================================ # 五、全局变量 # ============================================================ RUN_START_TIME="" RUN_START_EPOCH="" SCRIPT_NAME="ipv6-ddns.sh" FINAL_EXIT_CODE=0 LAST_HTTP_STATUS="" LAST_RESPONSE_BODY="" LAST_CURL_RC="" TOTAL_COUNT=0 UPDATED_COUNT=0 SKIPPED_COUNT=0 FAILED_COUNT=0 # ============================================================ # 六、基础函数 # ============================================================ now_ts() { "$DATE_BIN" '+%Y-%m-%d %H:%M:%S' 2>/dev/null } now_epoch() { "$DATE_BIN" '+%s' 2>/dev/null } print_line() { printf '%s\n' "$*" } log_line() { LEVEL="$1" shift TS="$(now_ts)" [ -n "$TS" ] || TS="unknown-time" printf '[%s] %-5s %s\n' "$TS" "$LEVEL" "$*" >> "$RUN_LOG" } log_info() { log_line "INFO" "$*" } log_warn() { log_line "WARN" "$*" } log_error() { log_line "ERROR" "$*" } log_debug() { if [ "$DEBUG" = "1" ]; then log_line "DEBUG" "$*" fi } log_multiline() { PREFIX="$1" CONTENT="$2" if [ -n "$CONTENT" ]; then print_line "$CONTENT" | while IFS= read -r L || [ -n "$L" ]; do log_info "$PREFIX$L" done else log_info "${PREFIX}<empty>" fi } is_placeholder_secret() { case "$1" in *请把参考脚本中的AK填到这里*|*请把参考脚本中的SK填到这里*|"") return 0 ;; *) return 1 ;; esac } mask_text() { VALUE="$1" LEN=${#VALUE} if [ "$LEN" -le 8 ]; then printf '******' else HEAD=$(printf '%s' "$VALUE" | cut -c 1-4) TAIL=$(printf '%s' "$VALUE" | cut -c "$((LEN - 3))"-"$LEN") printf '%s******%s' "$HEAD" "$TAIL" fi } # ============================================================ # 七、日志动作:插入主日志顶部 + 清理旧日志 # ============================================================ prune_log_by_days() { [ "$LOG_RETENTION_DAYS" -gt 0 ] 2>/dev/null || return 0 [ -f "$LOG_FILE" ] || return 0 NOW="$(now_epoch)" [ -n "$NOW" ] || return 0 CUTOFF=$((NOW - LOG_RETENTION_DAYS * 86400)) PRUNED="$RUNTIME_DIR/ipv6-ddns.pruned.$$.log" "$AWK_BIN" -v cutoff="$CUTOFF" ' BEGIN { inside = 0 block = "" keep = 1 } $0 == "##### ACTION_BLOCK_BEGIN #####" { if (inside == 1 && keep == 1) { printf "%s", block } inside = 1 block = $0 "\n" keep = 1 next } inside == 1 { block = block $0 "\n" if ($1 == "LOG_EPOCH:") { if (($2 + 0) < cutoff) { keep = 0 } } if ($0 == "##### ACTION_BLOCK_END #####") { if (keep == 1) { printf "%s", block } inside = 0 block = "" keep = 1 } next } { print } END { if (inside == 1 && keep == 1) { printf "%s", block } } ' "$LOG_FILE" > "$PRUNED" 2>/dev/null if [ -f "$PRUNED" ]; then "$MV_BIN" "$PRUNED" "$LOG_FILE" 2>/dev/null fi } prepend_run_log_to_main_log() { TMP_LOG="$RUNTIME_DIR/ipv6-ddns.merged.$$.log" END_TIME="$(now_ts)" { print_line "##### ACTION_BLOCK_BEGIN #####" print_line "============================================================" print_line "===== $RUN_START_TIME | IPv6-DDNS同步 | START =====" print_line "============================================================" print_line "LOG_EPOCH: $RUN_START_EPOCH" print_line "[$RUN_START_TIME] ACTION : IPv6-DDNS同步" print_line "[$RUN_START_TIME] SCRIPT : $0" print_line "[$RUN_START_TIME] LOG : $LOG_FILE" print_line "[$RUN_START_TIME] ZONE : $ZONE_NAME" print_line "[$RUN_START_TIME] TYPE : $RECORD_TYPE" print_line "[$RUN_START_TIME] TTL : $TTL" print_line "[$RUN_START_TIME] INPUT : $NEW_IPV6" print_line "[$RUN_START_TIME] RESULT : exit_code=$FINAL_EXIT_CODE total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" print_line "------------------------------------------------------------" [ -f "$RUN_LOG" ] && "$CAT_BIN" "$RUN_LOG" print_line "------------------------------------------------------------" print_line "[$END_TIME] RESULT: exit_code=$FINAL_EXIT_CODE total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" print_line "============================================================" print_line "===== $END_TIME | IPv6-DDNS同步 | END =====" print_line "============================================================" print_line "##### ACTION_BLOCK_END #####" print_line "" [ -f "$LOG_FILE" ] && "$CAT_BIN" "$LOG_FILE" } > "$TMP_LOG" "$MV_BIN" "$TMP_LOG" "$LOG_FILE" 2>/dev/null prune_log_by_days } finish() { EXIT_CODE=$? FINAL_EXIT_CODE="$EXIT_CODE" if [ -f "$RUN_LOG" ]; then prepend_run_log_to_main_log "$RM_BIN" -f "$RUN_LOG" 2>/dev/null fi exit "$EXIT_CODE" } # ============================================================ # 八、签名函数:只沿用参考脚本里的 AK/SK 使用方式 # ============================================================ sha256_hex() { DATA="$1" printf '%s' "$DATA" | "$OPENSSL_BIN" dgst -sha256 -binary | "$XXD_BIN" -p -c 256 } hmac_sha256_hex() { KEY="$1" DATA="$2" printf '%s' "$DATA" | "$OPENSSL_BIN" dgst -sha256 -mac HMAC -macopt "key:${KEY}" -binary | "$XXD_BIN" -p -c 256 } is_retryable_http_status() { case "$1" in 000|408|429|500|502|503|504) return 0 ;; *) return 1 ;; esac } signed_request() { METHOD="$1" REQUEST_PATH="$2" CANONICAL_URI="$3" CANONICAL_QUERY="$4" BODY="$5" CONTENT_TYPE="$6" URL="https://${HOST}${REQUEST_PATH}" if [ -n "$CANONICAL_QUERY" ]; then URL="${URL}?${CANONICAL_QUERY}" fi ATTEMPT=1 [ "$HTTP_MAX_RETRIES" -gt 0 ] 2>/dev/null || HTTP_MAX_RETRIES=1 while [ "$ATTEMPT" -le "$HTTP_MAX_RETRIES" ]; do X_SDK_DATE="$(TZ=UTC "$DATE_BIN" +%Y%m%dT%H%M%SZ 2>/dev/null)" PAYLOAD_HASH="$(sha256_hex "$BODY")" if [ -n "$CONTENT_TYPE" ]; then CANONICAL_HEADERS="content-type:${CONTENT_TYPE} host:${HOST} x-sdk-date:${X_SDK_DATE} " SIGNED_HEADERS="content-type;host;x-sdk-date" else CANONICAL_HEADERS="host:${HOST} x-sdk-date:${X_SDK_DATE} " SIGNED_HEADERS="host;x-sdk-date" fi CANONICAL_REQUEST="${METHOD} ${CANONICAL_URI} ${CANONICAL_QUERY} ${CANONICAL_HEADERS} ${SIGNED_HEADERS} ${PAYLOAD_HASH}" HASHED_CANONICAL_REQUEST="$(sha256_hex "$CANONICAL_REQUEST")" STRING_TO_SIGN="SDK-HMAC-SHA256 ${X_SDK_DATE} ${HASHED_CANONICAL_REQUEST}" SIGNATURE="$(hmac_sha256_hex "$SK" "$STRING_TO_SIGN")" AUTHORIZATION="SDK-HMAC-SHA256 Access=${AK}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}" log_info "HTTP 请求开始: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} method=$METHOD url=$URL" log_debug "X-Sdk-Date=$X_SDK_DATE" log_debug "SignedHeaders=$SIGNED_HEADERS" log_debug "PayloadHash=$PAYLOAD_HASH" log_debug "CanonicalRequest=$(printf '%s\n' "$CANONICAL_REQUEST" | "$SED_BIN" ':a;N;$!ba;s/\n/|/g')" [ -n "$BODY" ] && log_debug "RequestBody=$BODY" if [ -n "$CONTENT_TYPE" ]; then RESPONSE="$( "$CURL_BIN" -sS \ --connect-timeout 10 \ --max-time 30 \ -w '\n__HTTP_STATUS__:%{http_code}' \ -X "$METHOD" \ "$URL" \ -H "Host: ${HOST}" \ -H "X-Sdk-Date: ${X_SDK_DATE}" \ -H "Authorization: ${AUTHORIZATION}" \ -H "Content-Type: ${CONTENT_TYPE}" \ --data "$BODY" 2>&1 )" CURL_RC="$?" else RESPONSE="$( "$CURL_BIN" -sS \ --connect-timeout 10 \ --max-time 30 \ -w '\n__HTTP_STATUS__:%{http_code}' \ -X "$METHOD" \ "$URL" \ -H "Host: ${HOST}" \ -H "X-Sdk-Date: ${X_SDK_DATE}" \ -H "Authorization: ${AUTHORIZATION}" 2>&1 )" CURL_RC="$?" fi LAST_CURL_RC="$CURL_RC" if [ "$CURL_RC" != "0" ]; then LAST_HTTP_STATUS="CURL_ERROR" LAST_RESPONSE_BODY="$RESPONSE" log_error "HTTP 请求失败: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} curl_rc=$CURL_RC method=$METHOD url=$URL" log_multiline "curl输出: " "$RESPONSE" if [ "$ATTEMPT" -lt "$HTTP_MAX_RETRIES" ]; then SLEEP_SECONDS=$((HTTP_RETRY_BASE_SLEEP_SECONDS * ATTEMPT)) log_warn "HTTP 请求将重试: ${SLEEP_SECONDS}s 后进行第 $((ATTEMPT + 1)) 次尝试" sleep "$SLEEP_SECONDS" ATTEMPT=$((ATTEMPT + 1)) continue fi log_error "HTTP 请求失败且已达到最大重试次数: $HTTP_MAX_RETRIES" return 1 fi LAST_HTTP_STATUS="$(printf '%s' "$RESPONSE" | "$SED_BIN" -n 's/^__HTTP_STATUS__://p' | "$TAIL_BIN" -n 1)" LAST_RESPONSE_BODY="$(printf '%s' "$RESPONSE" | "$SED_BIN" '/^__HTTP_STATUS__:/d')" [ -n "$LAST_HTTP_STATUS" ] || LAST_HTTP_STATUS="000" log_info "HTTP 请求结束: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} status=${LAST_HTTP_STATUS} method=$METHOD url=$URL" log_debug "ResponseBody=$LAST_RESPONSE_BODY" if is_retryable_http_status "$LAST_HTTP_STATUS" && [ "$ATTEMPT" -lt "$HTTP_MAX_RETRIES" ]; then SLEEP_SECONDS=$((HTTP_RETRY_BASE_SLEEP_SECONDS * ATTEMPT)) log_warn "HTTP 状态可能是临时错误: status=${LAST_HTTP_STATUS},${SLEEP_SECONDS}s 后进行第 $((ATTEMPT + 1)) 次尝试" sleep "$SLEEP_SECONDS" ATTEMPT=$((ATTEMPT + 1)) continue fi return 0 done return 1 } # ============================================================ # 九、华为云 DNS API 操作函数 # ============================================================ get_zone_id() { signed_request \ "GET" \ "/v2/zones" \ "/v2/zones/" \ "limit=500&type=public" \ "" \ "" if [ "$LAST_HTTP_STATUS" != "200" ]; then log_error "查询 ZONE_ID 失败: HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 fi ZONE_ID="$(printf '%s' "$LAST_RESPONSE_BODY" \ | "$SED_BIN" 's/},{/},\ {/g' \ | "$GREP_BIN" -F "\"name\":\"${ZONE_NAME}\"" \ | "$SED_BIN" -n 's/.*"id":"\([^"]*\)".*/\1/p' \ | "$TAIL_BIN" -n 1)" if [ -n "$ZONE_ID" ]; then log_info "解析到 ZONE_ID: $ZONE_ID" return 0 fi log_error "没有从华为云 zones 响应中找到 ZONE_NAME=$ZONE_NAME" return 1 } get_record_line() { G_ZONE_ID="$1" G_RECORD_NAME="$2" signed_request \ "GET" \ "/v2.1/zones/${G_ZONE_ID}/recordsets" \ "/v2.1/zones/${G_ZONE_ID}/recordsets/" \ "limit=500" \ "" \ "" if [ "$LAST_HTTP_STATUS" != "200" ]; then log_error "查询 recordsets 失败: record=$G_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 fi MATCH_LINE="$(printf '%s' "$LAST_RESPONSE_BODY" \ | "$SED_BIN" 's/},{/},\ {/g' \ | "$GREP_BIN" -F "\"name\":\"${G_RECORD_NAME}\"" \ | "$GREP_BIN" -F "\"type\":\"${RECORD_TYPE}\"" \ | "$TAIL_BIN" -n 1)" printf '%s' "$MATCH_LINE" return 0 } parse_recordset_id() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"id":"\([^"]*\)".*/\1/p' | "$TAIL_BIN" -n 1 } parse_record_ipv6() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"records":\["\([^"]*\)"\].*/\1/p' | "$TAIL_BIN" -n 1 } parse_record_ttl() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"ttl":\([0-9]*\).*/\1/p' | "$TAIL_BIN" -n 1 } update_recordset() { U_ZONE_ID="$1" U_RECORDSET_ID="$2" U_RECORD_NAME="$3" U_NEW_IPV6="$4" BODY="{\"name\":\"${U_RECORD_NAME}\",\"type\":\"${RECORD_TYPE}\",\"ttl\":${TTL},\"records\":[\"${U_NEW_IPV6}\"]}" log_info "准备更新华为云记录: record=$U_RECORD_NAME recordset_id=$U_RECORDSET_ID" log_info "更新目标 IPv6: $U_NEW_IPV6" log_debug "更新请求 body=$BODY" signed_request \ "PUT" \ "/v2.1/zones/${U_ZONE_ID}/recordsets/${U_RECORDSET_ID}" \ "/v2.1/zones/${U_ZONE_ID}/recordsets/${U_RECORDSET_ID}/" \ "" \ "$BODY" \ "application/json" if [ "$LAST_HTTP_STATUS" = "202" ] || [ "$LAST_HTTP_STATUS" = "200" ]; then log_info "更新请求已被华为云接受: record=$U_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_debug "更新响应: $LAST_RESPONSE_BODY" return 0 fi log_error "更新记录失败: record=$U_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 } # ============================================================ # 十、环境检查与参数检查 # ============================================================ prepare_runtime() { "$MKDIR_BIN" -p "$RUNTIME_DIR" 2>/dev/null touch "$RUN_LOG" 2>/dev/null touch "$LOG_FILE" 2>/dev/null RUN_START_TIME="$(now_ts)" RUN_START_EPOCH="$(now_epoch)" [ -n "$RUN_START_TIME" ] || RUN_START_TIME="unknown-time" [ -n "$RUN_START_EPOCH" ] || RUN_START_EPOCH="0" } check_environment() { log_info "脚本开始执行" log_info "接收到的 IPv6 参数: ${NEW_IPV6:-<empty>}" log_info "脚本路径: $0" log_info "日志文件: $LOG_FILE" log_info "运行目录: $RUNTIME_DIR" log_info "华为云 HOST: $HOST" log_info "主域名 ZONE_NAME: $ZONE_NAME" log_info "记录类型: $RECORD_TYPE" log_info "目标 TTL: $TTL" log_info "AK: $(mask_text "$AK")" log_info "SK: ******" if is_placeholder_secret "$AK" || is_placeholder_secret "$SK"; then log_error "AK/SK 尚未配置。请把参考脚本中的 AK/SK 填入本脚本,或通过 HUAWEICLOUD_AK/HUAWEICLOUD_SK 环境变量传入。" return 1 fi if [ ! -x "$OPENSSL_BIN" ]; then log_error "找不到 openssl: $OPENSSL_BIN" return 1 fi if [ ! -x "$XXD_BIN" ]; then log_error "找不到 xxd: $XXD_BIN" return 1 fi if [ ! -x "$CURL_BIN" ]; then log_error "找不到 curl: $CURL_BIN" return 1 fi log_info "工具检查通过: openssl=$OPENSSL_BIN" log_info "工具检查通过: xxd=$XXD_BIN" log_info "工具检查通过: curl=$CURL_BIN" if [ -z "$NEW_IPV6" ]; then log_error "没有收到 IPv6 参数。正确用法: sh $0 2409:xxxx:xxxx::xxxx" return 1 fi if ! printf '%s' "$NEW_IPV6" | "$GREP_BIN" -qiE '^[0-9a-f:]+$'; then log_error "IPv6 格式看起来不正确: $NEW_IPV6" return 1 fi case "$NEW_IPV6" in 2*) log_info "IPv6 参数基础校验通过: 以 2 开头" ;; *) log_warn "IPv6 参数不是 2 开头: $NEW_IPV6" log_warn "wifi-keeper.sh 正常情况下只会传入 2 开头的长期 IPv6,这里仍继续执行。" ;; esac return 0 } # ============================================================ # 十一、主流程 # ============================================================ process_one_record() { P_ZONE_ID="$1" P_RECORD_NAME="$2" TOTAL_COUNT=$((TOTAL_COUNT + 1)) log_info "------------------------------------------------------------" log_info "开始处理记录: $P_RECORD_NAME" RECORD_LINE="$(get_record_line "$P_ZONE_ID" "$P_RECORD_NAME")" GET_RECORD_RC="$?" if [ "$GET_RECORD_RC" != "0" ]; then log_error "查询目标 AAAA 记录失败,无法判断记录是否存在: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi if [ -z "$RECORD_LINE" ]; then log_error "华为云没有找到目标 AAAA 记录: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi log_info "华为云记录原始信息: $RECORD_LINE" RECORDSET_ID="$(parse_recordset_id "$RECORD_LINE")" REMOTE_IPV6="$(parse_record_ipv6 "$RECORD_LINE")" REMOTE_TTL="$(parse_record_ttl "$RECORD_LINE")" log_info "解析到 RECORDSET_ID: ${RECORDSET_ID:-<empty>}" log_info "从华为云读取到的原 IPv6: ${REMOTE_IPV6:-<empty>}" log_info "从华为云读取到的 TTL: ${REMOTE_TTL:-<unknown>}" log_info "wifi-keeper.sh 传入的新 IPv6: $NEW_IPV6" if [ -z "$RECORDSET_ID" ]; then log_error "无法解析 RECORDSET_ID,跳过记录: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi if [ "$REMOTE_IPV6" = "$NEW_IPV6" ]; then log_info "对比结果: IPv6 一致,不需要更新" log_info "跳过更新: $P_RECORD_NAME" SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) return 0 fi log_info "对比结果: IPv6 不一致,需要更新" log_info "变化详情: ${REMOTE_IPV6:-<empty>} -> $NEW_IPV6" if ! update_recordset "$P_ZONE_ID" "$RECORDSET_ID" "$P_RECORD_NAME" "$NEW_IPV6"; then FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi log_info "开始二次查询,验证华为云是否已经更新为新 IPv6" VERIFY_LINE="$(get_record_line "$P_ZONE_ID" "$P_RECORD_NAME")" VERIFY_RC="$?" if [ "$VERIFY_RC" != "0" ]; then log_error "二次查询失败,无法验证华为云是否已经更新: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi VERIFY_IPV6="$(parse_record_ipv6 "$VERIFY_LINE")" log_info "二次查询记录原始信息: ${VERIFY_LINE:-<empty>}" log_info "二次查询解析到的 IPv6: ${VERIFY_IPV6:-<empty>}" if [ "$VERIFY_IPV6" = "$NEW_IPV6" ]; then log_info "更新验证成功: $P_RECORD_NAME 已经是 $NEW_IPV6" UPDATED_COUNT=$((UPDATED_COUNT + 1)) return 0 fi log_error "更新验证失败: $P_RECORD_NAME 当前仍是 ${VERIFY_IPV6:-<empty>},期望 $NEW_IPV6" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 } main() { prepare_runtime trap finish EXIT if ! check_environment; then exit 1 fi log_info "开始查询华为云 ZONE_ID" if ! get_zone_id; then exit 1 fi if [ -z "$ZONE_ID" ]; then log_error "ZONE_ID 为空,无法继续" exit 1 fi log_info "ZONE_ID 查询成功: $ZONE_ID" log_info "开始处理 RECORD_NAMES 列表" log_multiline "目标记录: " "$RECORD_NAMES" RECORD_LIST_FILE="$RUNTIME_DIR/record-list.$$.txt" printf '%s\n' "$RECORD_NAMES" > "$RECORD_LIST_FILE" while IFS= read -r RECORD_NAME || [ -n "$RECORD_NAME" ]; do [ -z "$RECORD_NAME" ] && continue case "$RECORD_NAME" in \#*) continue ;; esac process_one_record "$ZONE_ID" "$RECORD_NAME" done < "$RECORD_LIST_FILE" "$RM_BIN" -f "$RECORD_LIST_FILE" 2>/dev/null log_info "全部记录处理完成: total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" if [ "$FAILED_COUNT" -gt 0 ]; then log_error "存在失败记录,脚本以失败状态退出" exit 1 fi log_info "脚本执行成功" exit 0 } main 4. 同步DDNS时,需要用到openssl,所以先安装termux。 4.1 执行命令 pkg update 可能会提示: No mirror or mirror group selected. You might want to select one by running 'termux-change-repo' Testing the available mirrors: [*] (10) https://packages-cf.termux.dev/apt/termux-main: ok [*] (1) https://mirror.freedif.org/termux/termux-main: ok [*] (1) https://mirror.nevacloud.com/applications/termux/termux-main: ok [*] (1) https://mirrors.krnk.org/apt/termux/termux-main: ok [*] (1) https://mirrors.saswata.cc/termux/termux-main: bad [*] (1) https://tmx.xvx.my.id/apt/termux-main: bad [*] (1) https://mirror.rinarin.dev/termux/termux-main: ok [*] (1) https://mirror.albony.in/termux/termux-main: ok [*] (1) https://mirror.meowsmp.net/termux/termux-main: bad [*] (1) https://mirrors.in.sahilister.net/termux/termux-main/: ok [*] (1) https://mirrors.ravidwivedi.in/termux/termux-main: ok [*] (1) https://linux.domainesia.com/applications/termux/termux-main: ok [*] (1) https://mirror.jeonnam.school/termux/termux-main: ok [*] (1) https://termux.niranjan.co/termux-main: ok [*] (1) https://mirrors.cbrx.io/apt/termux/termux-main: ok [*] (1) https://mirror.bardia.tech/termux/termux-main: bad [*] (1) https://mirror.textcord.xyz/termux/termux-main: ok [*] (1) https://mirror.twds.com.tw/termux/termux-main: ok [*] (1) https://mirrors.nguyenhoang.cloud/termux/termux-main: ok [*] (1) https://mirrors.zju.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.pku.edu.cn/termux/termux-main/: ok [*] (1) https://mirrors.cernet.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.hust.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.bfsu.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.cqupt.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirror.nyist.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirror.sjtu.edu.cn/termux/termux-main/: ok [*] (1) https://mirrors.sau.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.sdu.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.nju.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.aliyun.com/termux/termux-main: ok [*] (1) https://mirror.iscas.ac.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.sustech.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.ustc.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.de.sahilister.net/termux/termux-main: ok [*] (1) https://ro.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://mirrors.medzik.dev/termux/termux-main: bad [*] (1) https://mirror.leitecastro.com/termux/termux-main: bad [*] (1) https://mirror.mwt.me/termux/main: ok [*] (1) https://termux.librehat.com/apt/termux-main: ok [*] (1) https://ftp.fau.de/termux/termux-main: ok [*] (1) https://packages.termux.dev/apt/termux-main: ok [*] (1) https://termux.mentality.rip/termux-main: ok [*] (1) https://md.mirrors.hacktegic.com/termux/termux-main: bad [*] (1) https://mirror.termux.dev/termux-main: bad [*] (1) https://mirror.bouwhuis.network/termux/termux-main: ok [*] (1) https://ftp.agdsn.de/termux/termux-main: ok [*] (1) https://mirrors.cfe.re/termux/termux-main: bad [*] (1) https://is.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://termux.3san.dev/termux/termux-main: ok [*] (1) https://mirror.accum.se/mirror/termux.dev/termux-main: ok [*] (1) https://mirror.sunred.org/termux/termux-main: ok [*] (4) https://grimler.se/termux/termux-main: ok [*] (1) https://mirror.autkin.net/termux/termux-main: ok [*] (1) https://mirror.polido.pt/termux/termux-main: bad [*] (1) https://nl.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://termux.cdn.lumito.net/termux-main: ok [*] (1) https://mirrors.utermux.dev/termux/termux-main: ok [*] (1) https://mirror.quantum5.ca/termux/termux-main: ok [*] (1) https://gnlug.org/pub/termux/termux-main: ok [*] (1) https://mirror.mwt.me/termux/main: ok [*] (1) https://plug-mirror.rcac.purdue.edu/termux/termux-main: bad [*] (1) https://termux.danyael.xyz/termux/termux-main: ok [*] (1) https://mirror.vern.cc/termux/termux-main: bad [*] (1) https://mirror.csclub.uwaterloo.ca/termux/termux-main: ok [*] (1) https://dl.kcubeterm.com/termux-main: bad [*] (1) https://mirror.fcix.net/termux/termux-main: bad [*] (1) https://mirrors.middlendian.com/termux/termux-main: bad [*] (1) http://mirror.mephi.ru/termux/termux-main: ok [*] (1) https://repository.su/termux/termux-main/: bad Picking mirror: (40) /data/data/com.termux/files/usr/etc/termux/mirrors/europe/mirrors.de.sahilister.net Get:1 https://mirrors.de.sahilister.net/termux/termux-main stable InRelease [14.0 kB] Get:2 https://mirrors.de.sahilister.net/termux/termux-main stable/main aarch64 Packages [547 kB] Fetched 561 kB in 6s (91.4 kB/s) Reading package lists... Done Building dependency tree... Done 73 packages can be upgraded. Run 'apt list --upgradable' to see them. ~ $ 意思是:你当前 Termux 还没有固定选择一个软件源镜像,所以它自动测试了一堆可用镜像。 不用管,termux会自动选择可用源帮你更新。 4.2然后执行命令安装openssl: pkg install openssl openssl-tool curl vim -y 安装过程中,如果提示输入内容,默认输入Y就行。 如果安装过程缓慢,可以搭载全局节点,也可以切换源地址后重新执行命令安装。 [!success] 到此,ipv6的健康度检查和ddns自动同步问题就解决了。手机重启以后,wifi会自动连接,然后ipv6会做健康度检查,然后自动同步DDNS。 [!warning] 由于论坛的文本编辑框,内容输入过多的时候,就会变得非常卡,由于本期代码太多了,所以帖子写的很慢。有很多细节上的东西不太重要我就暂时不补充了。不管是 ipv6 的检查脚本还是 DDNS 的同步脚本,都有非常详细的日志记录,如果有什么问题,大家可以把日志发出来,到时候我看看 4 个帖子 - 4 位参与者 阅读完整话题
以前这些都是给人看的,啰嗦详细点无所谓,可以只挑关心的看。 现在全都变成 token 喂给 agent 了,在输入层有必要做压缩么? Claude 之类的 agent 有做这方面处理么?
以前这些都是给人看的,啰嗦详细点无所谓,可以只挑关心的看。 现在全都变成 token 喂给 agent 了,在输入层有必要做压缩么? Claude 之类的 agent 有做这方面处理么?
以前这些都是给人看的,啰嗦详细点无所谓,可以只挑关心的看。 现在全都变成 token 喂给 agent 了,在输入层有必要做压缩么? Claude 之类的 agent 有做这方面处理么?
[!warning]注:原帖超过编辑时限 从 【开源skill】被跨对话/跨agent恢复上下文反复折磨后,我做了个项目本地记忆skill【新版本前瞻】 继续讨论: [!todo]社区开源推广声明 本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 [!check] 版本更新提示:v0.4.0 现已正式发布 已安装的佬友请尽快使用 npx skills update recallloom 来更新使用新版本! 🧶 RecallLoom 让项目自己记住自己。 把背景、进展、关键决策和下一步留在项目文件里。换会话、换模型、换工具,下一次 AI 协作也能接上当前状态。 [!tip]如果觉得对您有帮助,欢迎STAR并推荐给更多佬友! 背景 Hi~ 各位佬友! 距离上次RecallLoom更新已经过去了一段时间,今天终于迎来了RecallLoom的最新V0.4.0版本的正式发布~ 这段时间,我在规划、升级新版本的同时,也在和佬友们交流对于RecallLoom的想法、心得、以及使用感受,非常感谢佬友们对本项目的大力支持,同时也很感谢发现问题、为项目迭代提供思路的佬友们!RecallLoom是与大家一同成长的。【 详细见 原帖 讨论 】 此外,我也在RecallLoom开发项目、自己的硕士学位论文项目中,真实接入了RecallLoom来辅助项目管理,进行了深度体验,个人感受是真的有在提高项目推进效率并降低了解释成本,我很高兴能和佬友们进一步分享。 给新认识的朋友们介绍一下 [!QUESTION] 为什么要做 RecallLoom ? [!fail] 在使用 Agent 推进长期项目协作时,最磨人的常常是开头要费精力解释项目。 我把上述叫做“重启税”。 不知道佬友是不是已经遇到过下面的情况: 换会话、换模型、换Agent,或隔几天回来,就要花时间重新告诉 AI “我在做什么”。 新的 Agent 工具能读到仓库里的文件,却不知道哪些事实 已经过期 、哪些结论 仍然有效 。 平台私有记忆(memory)、聊天记录和项目文件 分开存在 ,关键决策很难跟着项目一起走。 项目一做久,“为什么这么做”“现在什么是真的”“下一步该接哪里”最容易丢。 [!success] RecallLoom 就是为了无痛丝滑解决 “ 项目级记忆 ”和 “ 接力连续性 ” RecallLoom 从项目级别把本项目的背景、当前状态、关键决策、最近进展和下一步,保存在项目内的受控文件里。下一次换会话、换模型、换工具,或隔几天回来时,新的 AI 工具可以快速先接上当前事实真相,再决定要不要深入历史材料。 这让“记忆”从原来聊天开场时的临时解释,变成项目里 可读、可审、可继续维护 的工程资产,摆脱“重启税”的烦恼。 [!QUESTION] 怎么想到做 RecallLoom ? 其实我发现上述问题早就成为大家普遍的痛点,一开始我也在论坛里找各种各样的解决方案,其中不乏各种Agent级别的记忆系统,还有轻量级的一个markdown文档记录一切的方法。但是,我都觉得不太适合自己推进的一些长期项目。 就拿写论文举例,我是一个典型的经管生,我们的论文不仅涉及到研究问题、论证思路、假设、结论推导等,还涉及模型、数据、实证等过程。一套完整论文的诞生,需要敲定研究方案,确认假设与理论推导逻辑,设计变量定义、模型结构、参数,还要进行实证统计检验,这是个复杂、长期的项目,也需要经历反复推翻、验证、再推翻的过程。 如果每次都在一个巨长的session里一直聊下去,本身就不可持续,上下文爆满,反复compact模型幻觉飘到天上去,准确的对话历史也难以高质量召回。 如果每次新开一个对话,又不可能无线fork下去,又要费尽心思组织开场白,费一堆时间来解释来龙去脉,我做到哪了? 文字写作我喜欢用deepseek V4 pro,如果涉及代码、数据工作,我又要切到codex用GPT或者切到Claude Code,这时候又要解释一大堆背景了。 也是在探索过程中,慢慢确定了如今 RecallLoom 的形态和生态位 —— 它是一个轻量的、文件原生的 项目连续性接力 SKILL 30秒开始 1. 安装 RecallLoom 把下面命令复制到终端。 如果你不想处理命令细节,可以把整行交给 Agent 执行: npx skills add https://github.com/Frappucc1no/recall-loom --skill recallloom 之后需要更新时: npx skills update recallloom 2. 首次初始化(可选,仅首次) 如果这是 第一次 在当前项目使用 RecallLoom,需要先初始化项目记忆。 [!TIP]已有 .recallloom/ 的项目可以跳过这一步。 初始化需要用任意一种 显式 唤起方式: 如: 1. @recallloom 初始化当前项目 2. 请用 RecallLoom 接管这个项目 3. 请用 RecallLoom 初始化这个项目 4. rl-init [!info] 首次接入会在项目旁边建立 RecallLoom 的项目记忆目录,用来保存背景、进展、关键决策和下一步。 3. 日常使用: 自然语言继续 项目接入后,常用说法很自然: 如: 继续这个项目 先帮我恢复项目上下文 从上次停下的地方继续 记录今天的关键进展 [!info]在已接入的项目里, 继续这个项目 会让 RecallLoom 先执行恢复步骤:先读取项目记忆,再进入具体任务。 [!SUCCESS] 日常接力无需每次都写 @recallloom 。 4. 熟悉后可用短触发词 (可选) 这些词可以直接发给 AI 工具,作用类似更短的自然语言触发语: 直接输入 你想做什么 rl-init 初始化项目记忆 ,让 RecallLoom 接管当前项目 rl-resume 恢复项目背景、当前状态和下一步 rl-status 查看项目记忆是否完整、是否需要处理 rl-validate 检查连续性文件有没有结构问题 多数时候,说“继续这个项目”就够了;熟悉后再用短触发词提速。 真实效果 眼见为实,下面请看在我的【硕士学位论文】项目中的真实效果: [!WARNING]多图预警 ( Codex Desktop / GPT-5.5 ) [!check]prompt:“恢复论文进展” [!check]prompt:“梳理近一个月论文的推进进展,关键结论,以及我推翻了啥?” [!check]prompt:“上述是通过recallloom恢复的吗?其帮助有多大” 新版本更新了啥 [!TIP]V0.4.0 (2026-05-24) 更准确的恢复路径、更低摩擦的进展更新、更清楚的写入保护和工具边界 v0.4.0版本比较大的变化是:我优化了逻辑架构,吸收轻量“图记忆”的基本原理,但不至于做成重型“图记忆”数据库或RAG检索。 此举,使得RecallLoom现在能够对写入的记忆附带一个“关系路标”,它可以把与之关联的记忆打上路标,在记忆检索、召回、降级、过时记忆退出的时候,能够更加准确。真正意义上,使用文件原生的形态,以轻量化方案来缓解记忆关联的难题。 详细版本摘要如下: 更可靠的项目恢复 :优先读取当前状态、活跃判断和下一步,再按需进入背景材料和历史记录,减少旧信息干扰。 更顺手的进展记录 :更加优化的结构化记录,并提供记录后同步当前状态的标准路径,让“刚刚完成了什么”和“下一步接哪里”保持一致。 更安全的长期记忆写入 :加强修订检查、新鲜度检查、来源文件边界和临时草稿处理,降低把过期内容或不该沉淀的内容写入项目记忆的风险。 更清楚的多工具协作边界 :适配Codex、Claude Code、Gemini CLI、OpenCode 等Agent的最新边界,并提供相应的快捷命令入口,但项目事实仍保存在工作区内的 RecallLoom 文件中。 更清晰的说明 :README、USAGE、SKILL、package metadata 的职责更明确,安装、恢复、更新和可选 native wrappers 的入口更一致。 更严格的隐私与输出安全 :继续对私有路径、token 形态、元数据和外部来源摘要做 public-safe 处理,减少把本地环境细节带入输出的风险。 项目接力循环 RecallLoom 的核心模型可以概括成一个 典型项目记忆循环 : 特性与工程设计 [!info]本节详细内容请移步阅读 README [!TIP]RecallLoom 的价值——更短的恢复路径 少解释、少重读、少猜测,让 AI 工具先接上当前事实。 特性 带来的价值 低重启税 换会话、换模型或隔几天回来时,先恢复项目状态 更快接力 先看当前摘要、最近进展和下一步,快速进入状态 更省上下文 先读小而准的项目记忆,深查只在需要时发生 跨工具接力 换模型、换会话、换 AI 工具时,项目事实仍跟着工作区走 写入更稳 进展记录、当前摘要和校验动作有明确路径,降低把过期事实写回项目记忆的风险 文件化保存 记忆落在 Markdown / JSON 文件里,可读、可审、可迁移 Github链接 [!SUMMARY]帮我点个 STAR 吧! 这对我是个很好的帮助。 github.com GitHub - Frappucc1no/recall-loom: Portable continuity layer for long-running AI... Portable continuity layer for long-running AI projects across models, agents, and sessions. 致谢 感谢迭代过程中所有提供支持和建议的朋友们 感谢各大中转站的GPT资源(我好像花了25亿+Tokens) 1 个帖子 - 1 位参与者 阅读完整话题
今天安装了 qoderclicn ,运行的之后提示 Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience. 查了下 ubuntu 终端默认 256 色
今天安装了 qoderclicn ,运行的之后提示 Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience. 查了下 ubuntu 终端默认 256 色
盘点L站的徽章 长期更新!(2026.2.17更新) 文档共建 盘点 L 站的徽章长期更新!(2026.2.17 更新) [!warning] 注意! 做 Wiki 编辑的徽章就做,别把整个教程帖子内容覆盖成无意义的内容!!!!!!!!!! [!caution] 注意 … 看到了这个大佬 @Jenhy 发的很全,目前正在慢慢完成,还有其他渠道可以增加分数吗 3 个帖子 - 2 位参与者 阅读完整话题
今天安装了 qoderclicn ,运行的之后提示 Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience. 查了下 ubuntu 终端默认 256 色
今天安装了 qoderclicn ,运行的之后提示 Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience. 查了下 ubuntu 终端默认 256 色
今天安装了 qoderclicn ,运行的之后提示 Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience. 查了下 ubuntu 终端默认 256 色
[!warning] 文化宣导员 徽章开放获取通道了!想要的佬友尽快参与活动! 指路 【又520专场】你如何理解我们的社区文化 运营反馈 从 【520专场】你如何理解我们的社区文化 继续讨论: 一年了,还是没有对象?今天佬友再陪你过 520! 各位 L站 的 佬友 们,今天是个稍微有些特别的日子,应该充满爱的日子 。 【文化宣导员】 徽章再次登场,后续加入的佬友们机会来了!你是如何理解我们的社区文化的呢? 真诚 、友善 、团结 、专业 ,共建你我引以为荣之社区。 请… 全部徽章获取教程(参考) 盘点L站的徽章 长期更新!(2026.2.17更新) 文档共建 盘点 L 站的徽章长期更新!(2026.2.17 更新) [!warning] 注意! 做 Wiki 编辑的徽章就做,别把整个教程帖子内容覆盖成无意义的内容!!!!!!!!!! [!caution] 注意 … 4 个帖子 - 3 位参与者 阅读完整话题
今天早上登录服务器(宝塔面板)发现CPU 100%,以为是刚登录比较卡而已,过了一会,还是这样,就去终端运行 top 看了看,发现奇怪的一条: 2391782 1001 20 0 316396 270196 2480 S 197.7 14.6 10,29 xmrig 不懂这是个啥,就去问了GPT,说是一种名为 XMRig 的“门罗币(Monero)挖矿程序”。 还让我去查这些文件 运行 ls -l /proc/2391782/exe 输出: lrwxrwxrwx 1 1001 1001 0 May 12 03:20 /proc/2391782/exe -> /home/gotenberg/xmrig 看这个我就想起这个 gotenberg 是我docker部署的一个word文档转PDF的服务,也没怎么用过,就赶紧去停了,然后CPU一下就恢复正常了。 还不放心,在GPT的指导下看看有没有其他泄露到宿主机的问题,查了下,好像是没有,删除原来的那个镜像的所有文件,就这样了,实在也是不想重装。 没想到这些东西还会导致挖矿程序,当然也是小白不太懂这些 [!warning] 也可能这个服务有漏洞,也可能我是小白 GPT建议我不要暴露公网端口,最好用本地端口再反代 4 个帖子 - 3 位参与者 阅读完整话题
写在开头 FBI Warning⚠️:如果您没有使用过 ai 工具,没有相关的编程经验,或是对这个话题不敢兴趣,请您现在就退出当前页面。它将浪费你人生中宝贵的三分钟 友情提示:如果你想设计一个自己的 agent 或者想要深入理解 agent 如何高效运行,那么花 10 分钟理解本文会是你今年迄今为止对自己的时间做出的最值得的投资 想象一个项目工程,是做加法容易?还是减法容易?做一个通用 agent ,如何兼顾所有用户需求?如何能在简洁的前提下让一个有智慧的 agent 充分自举? GitHub: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 1. 核心问题:Agent 为什么跑着跑着就变蠢了? 做过 Agent 开发的应该都遇到过这个现象:Agent 在前几轮表现不错,但随着对话轮次增加,它开始丢约束、忘指令、重复犯错。 作者把这个问题归结为两个根本挑战: 挑战一:上下文爆炸。 每一轮交互都在往上下文里塞东西——工具定义、历史对话、工具返回值、检索到的记忆。这些内容在产生时各有用途,但对"下一步该做什么"的贡献参差不齐。无关内容不是被浪费那么简单,它会主动稀释模型的注意力,导致约束遗漏和幻觉。 挑战二:经验停滞。 如果 Agent 无法把成功经验沉淀下来,每次遇到类似任务都得从头探索。token 花了一堆,能力纹丝不动。作者管这叫 Stagnation Loop 。 这两个挑战的交汇点指向一个核心问题: LLM 的上下文到底应该塞什么? 2. 第一性原理:上下文信息密度最大化 GA 给出的答案是一个形式化的设计目标: D(C) = 决策相关信息量(C) / 上下文总长度(C) → max 翻译成人话:不追求上下文的长度,追求每一个 token 对当前决策的贡献密度。 这个目标拆开来看有两个维度: 完备性( Completeness ) :当前决策需要的信息必须显式出现在上下文中,不能让模型靠猜。 简洁性( Conciseness ) :无关和冗余的信息必须清除,让注意力聚焦在关键信号上。 作者提出了一个关键洞察: 完备性和简洁性之间的张力是结构性的,不是资源问题。 即使上下文窗口无限大,这个矛盾依然存在——因为加入更多"可能相关"的信息提升了完备性,却必然稀释注意力(削弱简洁性);而压缩提升了简洁性,却有丢失关键细节的风险(削弱完备性)。 所以 GA 的所有设计决策,本质上都是在这个结构性张力下做约束优化。 3. 为什么"上下文越长表现越差"——三重陷阱 这不是 GA 自己编的结论,是多篇论文验证过的现象。作者总结了三重相互强化的失效模式: 位置偏差( Lost-in-the-Middle ) :LLM 对上下文开头和结尾的信息利用率高,中间部分容易被"遗忘"。关键信息落在中间位置时,模型可能直接忽略。 注意力稀释( Attention Dilution ) :注意力是有限资源。无关内容越多,分配给每条关键信息的注意力越少。无关内容不是被浪费,而是主动干扰。 有效窗口远小于名义窗口 :一个标称 128K 的模型,真正能稳定推理的有效窗口可能只有几万 token 。随着上下文增长,推理能力逐渐退化。 这三者形成恶性循环:上下文膨胀 → 注意力稀释 → 位置偏差加剧 → 有效窗口收缩 → 系统倾向于注入更多"可能有用"的内容来补偿 → 上下文进一步膨胀。 核心启示:超过某个临界点后,增加更多上下文不仅无法提升性能,反而会降低表现。 4. GA 的系统性解法:四层信息密度优化 GA 不是靠一个 trick 解决问题,而是在信息生命周期的四个阶段分别做优化: 4.1 最小原子工具集——减少"先天噪声" 工具定义是上下文中 每轮都要重复支付的固定成本 。GA 的策略是极端克制:只保留 9 个原子工具。 为什么不是越多越好?作者指出工具膨胀有两层代价: Prompt 层 :每增加一个工具,就要在上下文中注入 Schema (名称、描述、参数类型)。53 个工具的 Schema 可能消耗上万 token ,而且每轮重复。 Policy 层 :工具越多,动作空间越大,选择歧义越高。比如"读文件"这个操作,如果同时有 FileReadTool 、GrepTool 、BashTool(cat),模型需要理解三者的微妙差异才能正确选择。 GA 的 9 个工具覆盖五大能力类:文件操作( file_read / file_write / file_patch )、代码执行( code_run )、网页交互( web_scan / web_execute_js )、记忆管理( update_working_checkpoint / start_long_term_update )、人机协作( ask_user )。 关键设计: code_run 是万能逃生舱。任何 9 个工具覆盖不到的长尾需求,都可以通过写代码来实现。这意味着工具集不需要为每个边缘场景增加专用工具——保持了工具层的极简,同时不牺牲能力上限。 code_run 本质上是图灵完备的! 实际使用分布(论文数据):code_run 34.4%、file_read 31.2%、update_working_checkpoint 17.2%,三个工具覆盖了 82.8% 的调用。 4.2 分层按需记忆——只加载"当前需要的" 传统方案要么不保留历史(每次从零开始),要么全量追加(上下文爆炸)。GA 用了一个四层架构: 层级 定位 是否 always-on 典型大小 L1 索引层 目录卡片,告诉 Agent "有哪些知识可用" 是 ~200 token L2 事实层 环境事实、用户偏好、服务器信息 否,按需 file_read 数百~数千 token L3 SOP 层 标准操作流程、技能脚本 否,按需 file_read 每个 SOP 数百 token L4 原始日志 完整对话历史,用于追溯和审计 否,极少访问 无限增长 核心机制: L1 始终在上下文中(成本极低),L2-L4 只在需要时才被加载。 这就像图书馆——你不会把所有书搬到桌上才开始工作,而是先查目录( L1 ),再去书架取需要的那本( L2/L3 )。 作者的消融实验验证了这个设计的有效性: 记忆配置 记忆大小( token ) 任务成功率 TSR No memory 0 52.44% Full memory (全量注入) 575 52.44% GA 分层记忆 165 66.48% 165 token 的分层记忆达到了 575 token 全量注入的 1.27 倍成功率。 全量注入反而跟没有记忆一样——因为无关信息稀释了注意力。这是"上下文越长表现越差"的直接实证。 4.3 上下文截断与压缩——主动瘦身 即使工具和记忆都控制住了,对话轮次增加后上下文仍会膨胀。GA 用四阶段压缩流水线处理: 工具返回值截断 :code_run 输出超长时只保留头尾 历史轮次压缩 :早期对话轮次被摘要化 消息驱逐 :超出预算的最旧消息被移除 工作记忆锚点注入 :通过 update_working_checkpoint 工具,Agent 主动把关键中间状态写入一个始终可见的锚点,防止被压缩丢失 第 4 点是个巧妙的设计——Agent 自己决定什么信息值得"钉住",而不是靠启发式规则猜测。 4.4 反思驱动的自我进化——让未来的上下文更精炼 GA 能把成功的任务经验蒸馏为 SOP 存入 L3 。下次遇到类似任务时,不需要在上下文中重新探索整个解决方案,直接调用之前积累的精炼经验。 这解决了"挑战二:经验停滞"。没有进化机制时,Agent 面临两难:要么重放更长的探索过程(削弱简洁性),要么从更短但信息不足的提示词开始(削弱完备性)。自我进化打破了这个两难——把冗长的探索轨迹压缩为紧凑的可复用知识。 5. 架构实现:92 行的 Agent Loop GA 的主循环只有约 92 行,结构是标准的 perceive-think-act: while not done: context = assemble(system_prompt, always_on_memory, tools, history) response = llm.chat(context) if response.has_tool_calls: results = execute(response.tool_calls) history.append(results) else: done = True 整个系统由 4 个文件构成: agent_loop.py (主循环)、 ga.py (工具实现)、 agentmain.py (入口 + 前端适配)、 llmcore.py ( LLM 调用封装)。总计约 3300 行。 这个极简架构的好处是: 没有隐藏的复杂度 。没有事件总线、没有调度守护进程、没有专用子 Agent 管理器。子 Agent 并行、定时任务、看门狗监控这些"高级功能",全部通过 9 个基础工具的组合涌现出来。 6. 约束下的涌现:三个原语长出整个生态 这是 GA 设计中我觉得最有意思的部分。 传统框架做高级功能的方式是"功能内置":需要子 Agent ?加一个 SubAgent Manager 。需要定时任务?加一个 Scheduler Daemon 。需要事件驱动?加一个 Event Bus 。每个新功能都带来新的接口、新的配置、新的故障模式。系统复杂度线性增长。 GA 走了另一条路: 不内置任何高级功能,只提供三个足够通用的原语,让高级行为从组合中涌现。 三个基础原语 原语 本质 一句话描述 自托管 CLI 入口点 Agent 就是一个命令行程序 任何能调用命令行的程序都能调用 GA——包括 GA 自己 文件协议 目录约定的跨进程通信 input.txt 送任务、output.txt 流结果、reply.txt 续对话、stop.txt 终止 反射模式 轮询 + 热重载 外部脚本定义触发条件,运行时周期性求值,脚本修改后自动重载无需重启 涌现出的高级行为 子 Agent 并行分发 :父 Agent 通过 code_run 启动自己的另一个实例,传入不同的 task-dir 。父子是同构进程——不存在特权的子 Agent 运行时对象,双方遵循完全相同的文件协议。上下文天然隔离(独立进程 = 独立内存空间),不需要复杂的状态管理来区分"哪些历史属于哪个子任务"。 看门狗监控 :反射模式 + 一个检测环境变化的触发脚本。当脚本返回非空字符串时,该字符串被作为任务分发到标准流水线。不需要专门的监控框架。 定时任务调度 :反射模式 + 一个检查时间条件的触发脚本。跟看门狗共享完全相同的底层机制,区别仅在于脚本内容。 自主空闲行为 :反射模式 + 一个"当前无任务"的触发条件。Agent 在空闲时自动执行预设的探索或维护任务。 为什么这能工作 这里的"涌现"不是物理学意义上的不可预测——而是工程意义上的: 当基础组件足够通用且组合成本足够低时,设计者未预先规划的功能可以在需要时被轻松实现。 关键不在于"意外",而在于"低成本"。传统框架每加一个高级功能需要修改核心代码、添加新模块、更新文档。GA 只需要写一个触发脚本或一段 code_run 调用——核心代码一行不改。 作者给出的数据:GA 核心代码 3300 行( Agent Loop 仅 92 行),而实现了子 Agent 并行、看门狗、定时调度、自主行为等全部高级功能。作为对比,OpenClaw 的代码量约 530,000 行——160 倍以上。这不是说代码少就一定好,但它说明了一件事: 当原语选对了,复杂行为不需要复杂实现。 7. Benchmark 数据 作者在 WebArena 、OSWorld 等标准 benchmark 上做了评测(数据来源:论文 Table 5 ): 指标 GA 总 Token 消耗 188,829 任务成功率( TSR ) 66.48% Token 消耗对比(同一组任务): 系统 Token 消耗 相对 GA GA 188,829 1x Claude Code 538,207 2.85x OpenClaw 633,498 3.35x 完整提示词长度对比(安装 20 个技能后,对 "Hello" 的响应): 系统 Full Prompt Length ( token ) OpenClaw 43,321 CodeX 23,932 Claude Code 22,821 GA 2,298 8. 从 Prompt Engineering 到 Context Engineering 作者提了一个我觉得很有价值的视角转变: Prompt Engineering 优化的是"一句话怎么说" Context Engineering 优化的是"每一轮对话中,模型看到的所有信息应该是什么" 在 Agent 场景下,上下文不只是用户指令,还包含工具定义、历史对话、记忆内容、工具返回值等多种组件。如何系统性地管理这些组件的注入、压缩和替换,才是决定 Agent 长期表现的关键。 GA 通过工具层、记忆层、压缩层和进化层四个维度,把 Context Engineering 落实为具体的系统机制。这不是一个 prompt 写得好不好的问题,而是一个系统架构问题。 9. 我的使用体感和局限 用了一段时间后的观察: 上下文 30K 的硬限制是双刃剑。 好处是 Agent 不会因为对话太长而变蠢;代价是单轮无法处理超大文件,需要分段读取。 记忆系统完全基于文件,没有向量检索。 SOP 数量特别多时,L1 索引的命中率可能下降。但对于个人使用场景(几十个 SOP ),目前没遇到问题。 code_run 是万能的,也是危险的。 能执行任意代码意味着安全性完全依赖部署环境的隔离。 SOP 质量很重要。 写得好的 SOP 能让 Agent 一步到位;写得不好的会误导。这是一个需要用户投入的地方。 多 Agent 协作通过进程 + 文件实现,没有结构化通信协议。 简单场景够用,复杂协作场景调试不太方便。 10. 总结 GA 的核心贡献不是某个具体的 trick ,而是一套完整的设计哲学: 在完备性与简洁性的结构性张力下,通过四层机制系统性地最大化上下文信息密度。 它证明了一件事:Agent 的能力上限不取决于上下文能塞多少字,而取决于在有限 token 预算里,能装进多少真正对当前决策有用的信息。 如果你正在做 Agent 开发,不管用不用 GA ,它的设计思路都值得参考——特别是"信息密度"这个视角,对 prompt 设计、记忆系统设计、工具集设计都有直接的指导意义。 项目地址: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 写在最后 在上一个帖子发出后,受到了广泛的关注,深感荣幸,所以火速加更! 贴几条热心的 v 友对我善意的人身攻击,我深刻的认识到了我的不足,并意识到自己 too young too simple ,sometimes naive 。 我做出如下承诺: 1 、今后 只写提示词,不写文章。 2 、在提示词中要求文章内容 去学术化,去叽里咕噜化,去指标化。 3 、更广泛的听取大家的批评,了解 V2EX 的社区规范,学习落实 v 站大佬的悉心教导。保证做到: “余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。故余虽愚,卒获有所闻。” 另外再回复一下这条: 本人再次重申,本人为某 top3 高校在读博士,大模型方向,于 3 月中旬刚刚恢复单身,欢迎感兴趣的异性 v 友留下联系方式,谢谢! 最后附上上面这篇文章的提示词: 欢迎大家用 ga 写点公众号文章或者软文吹 claude code 和 codex ,给自己挣点外快!
写在开头 FBI Warning⚠️:如果您没有使用过 ai 工具,没有相关的编程经验,或是对这个话题不敢兴趣,请您现在就退出当前页面。它将浪费你人生中宝贵的三分钟 友情提示:如果你想设计一个自己的 agent 或者想要深入理解 agent 如何高效运行,那么花 10 分钟理解本文会是你今年迄今为止对自己的时间做出的最值得的投资 想象一个项目工程,是做加法容易?还是减法容易?做一个通用 agent ,如何兼顾所有用户需求?如何能在简洁的前提下让一个有智慧的 agent 充分自举? GitHub: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 1. 核心问题:Agent 为什么跑着跑着就变蠢了? 做过 Agent 开发的应该都遇到过这个现象:Agent 在前几轮表现不错,但随着对话轮次增加,它开始丢约束、忘指令、重复犯错。 作者把这个问题归结为两个根本挑战: 挑战一:上下文爆炸。 每一轮交互都在往上下文里塞东西——工具定义、历史对话、工具返回值、检索到的记忆。这些内容在产生时各有用途,但对"下一步该做什么"的贡献参差不齐。无关内容不是被浪费那么简单,它会主动稀释模型的注意力,导致约束遗漏和幻觉。 挑战二:经验停滞。 如果 Agent 无法把成功经验沉淀下来,每次遇到类似任务都得从头探索。token 花了一堆,能力纹丝不动。作者管这叫 Stagnation Loop 。 这两个挑战的交汇点指向一个核心问题: LLM 的上下文到底应该塞什么? 2. 第一性原理:上下文信息密度最大化 GA 给出的答案是一个形式化的设计目标: D(C) = 决策相关信息量(C) / 上下文总长度(C) → max 翻译成人话:不追求上下文的长度,追求每一个 token 对当前决策的贡献密度。 这个目标拆开来看有两个维度: 完备性( Completeness ) :当前决策需要的信息必须显式出现在上下文中,不能让模型靠猜。 简洁性( Conciseness ) :无关和冗余的信息必须清除,让注意力聚焦在关键信号上。 作者提出了一个关键洞察: 完备性和简洁性之间的张力是结构性的,不是资源问题。 即使上下文窗口无限大,这个矛盾依然存在——因为加入更多"可能相关"的信息提升了完备性,却必然稀释注意力(削弱简洁性);而压缩提升了简洁性,却有丢失关键细节的风险(削弱完备性)。 所以 GA 的所有设计决策,本质上都是在这个结构性张力下做约束优化。 3. 为什么"上下文越长表现越差"——三重陷阱 这不是 GA 自己编的结论,是多篇论文验证过的现象。作者总结了三重相互强化的失效模式: 位置偏差( Lost-in-the-Middle ) :LLM 对上下文开头和结尾的信息利用率高,中间部分容易被"遗忘"。关键信息落在中间位置时,模型可能直接忽略。 注意力稀释( Attention Dilution ) :注意力是有限资源。无关内容越多,分配给每条关键信息的注意力越少。无关内容不是被浪费,而是主动干扰。 有效窗口远小于名义窗口 :一个标称 128K 的模型,真正能稳定推理的有效窗口可能只有几万 token 。随着上下文增长,推理能力逐渐退化。 这三者形成恶性循环:上下文膨胀 → 注意力稀释 → 位置偏差加剧 → 有效窗口收缩 → 系统倾向于注入更多"可能有用"的内容来补偿 → 上下文进一步膨胀。 核心启示:超过某个临界点后,增加更多上下文不仅无法提升性能,反而会降低表现。 4. GA 的系统性解法:四层信息密度优化 GA 不是靠一个 trick 解决问题,而是在信息生命周期的四个阶段分别做优化: 4.1 最小原子工具集——减少"先天噪声" 工具定义是上下文中 每轮都要重复支付的固定成本 。GA 的策略是极端克制:只保留 9 个原子工具。 为什么不是越多越好?作者指出工具膨胀有两层代价: Prompt 层 :每增加一个工具,就要在上下文中注入 Schema (名称、描述、参数类型)。53 个工具的 Schema 可能消耗上万 token ,而且每轮重复。 Policy 层 :工具越多,动作空间越大,选择歧义越高。比如"读文件"这个操作,如果同时有 FileReadTool 、GrepTool 、BashTool(cat),模型需要理解三者的微妙差异才能正确选择。 GA 的 9 个工具覆盖五大能力类:文件操作( file_read / file_write / file_patch )、代码执行( code_run )、网页交互( web_scan / web_execute_js )、记忆管理( update_working_checkpoint / start_long_term_update )、人机协作( ask_user )。 关键设计: code_run 是万能逃生舱。任何 9 个工具覆盖不到的长尾需求,都可以通过写代码来实现。这意味着工具集不需要为每个边缘场景增加专用工具——保持了工具层的极简,同时不牺牲能力上限。 code_run 本质上是图灵完备的! 实际使用分布(论文数据):code_run 34.4%、file_read 31.2%、update_working_checkpoint 17.2%,三个工具覆盖了 82.8% 的调用。 4.2 分层按需记忆——只加载"当前需要的" 传统方案要么不保留历史(每次从零开始),要么全量追加(上下文爆炸)。GA 用了一个四层架构: 层级 定位 是否 always-on 典型大小 L1 索引层 目录卡片,告诉 Agent "有哪些知识可用" 是 ~200 token L2 事实层 环境事实、用户偏好、服务器信息 否,按需 file_read 数百~数千 token L3 SOP 层 标准操作流程、技能脚本 否,按需 file_read 每个 SOP 数百 token L4 原始日志 完整对话历史,用于追溯和审计 否,极少访问 无限增长 核心机制: L1 始终在上下文中(成本极低),L2-L4 只在需要时才被加载。 这就像图书馆——你不会把所有书搬到桌上才开始工作,而是先查目录( L1 ),再去书架取需要的那本( L2/L3 )。 作者的消融实验验证了这个设计的有效性: 记忆配置 记忆大小( token ) 任务成功率 TSR No memory 0 52.44% Full memory (全量注入) 575 52.44% GA 分层记忆 165 66.48% 165 token 的分层记忆达到了 575 token 全量注入的 1.27 倍成功率。 全量注入反而跟没有记忆一样——因为无关信息稀释了注意力。这是"上下文越长表现越差"的直接实证。 4.3 上下文截断与压缩——主动瘦身 即使工具和记忆都控制住了,对话轮次增加后上下文仍会膨胀。GA 用四阶段压缩流水线处理: 工具返回值截断 :code_run 输出超长时只保留头尾 历史轮次压缩 :早期对话轮次被摘要化 消息驱逐 :超出预算的最旧消息被移除 工作记忆锚点注入 :通过 update_working_checkpoint 工具,Agent 主动把关键中间状态写入一个始终可见的锚点,防止被压缩丢失 第 4 点是个巧妙的设计——Agent 自己决定什么信息值得"钉住",而不是靠启发式规则猜测。 4.4 反思驱动的自我进化——让未来的上下文更精炼 GA 能把成功的任务经验蒸馏为 SOP 存入 L3 。下次遇到类似任务时,不需要在上下文中重新探索整个解决方案,直接调用之前积累的精炼经验。 这解决了"挑战二:经验停滞"。没有进化机制时,Agent 面临两难:要么重放更长的探索过程(削弱简洁性),要么从更短但信息不足的提示词开始(削弱完备性)。自我进化打破了这个两难——把冗长的探索轨迹压缩为紧凑的可复用知识。 5. 架构实现:92 行的 Agent Loop GA 的主循环只有约 92 行,结构是标准的 perceive-think-act: while not done: context = assemble(system_prompt, always_on_memory, tools, history) response = llm.chat(context) if response.has_tool_calls: results = execute(response.tool_calls) history.append(results) else: done = True 整个系统由 4 个文件构成: agent_loop.py (主循环)、 ga.py (工具实现)、 agentmain.py (入口 + 前端适配)、 llmcore.py ( LLM 调用封装)。总计约 3300 行。 这个极简架构的好处是: 没有隐藏的复杂度 。没有事件总线、没有调度守护进程、没有专用子 Agent 管理器。子 Agent 并行、定时任务、看门狗监控这些"高级功能",全部通过 9 个基础工具的组合涌现出来。 6. 约束下的涌现:三个原语长出整个生态 这是 GA 设计中我觉得最有意思的部分。 传统框架做高级功能的方式是"功能内置":需要子 Agent ?加一个 SubAgent Manager 。需要定时任务?加一个 Scheduler Daemon 。需要事件驱动?加一个 Event Bus 。每个新功能都带来新的接口、新的配置、新的故障模式。系统复杂度线性增长。 GA 走了另一条路: 不内置任何高级功能,只提供三个足够通用的原语,让高级行为从组合中涌现。 三个基础原语 原语 本质 一句话描述 自托管 CLI 入口点 Agent 就是一个命令行程序 任何能调用命令行的程序都能调用 GA——包括 GA 自己 文件协议 目录约定的跨进程通信 input.txt 送任务、output.txt 流结果、reply.txt 续对话、stop.txt 终止 反射模式 轮询 + 热重载 外部脚本定义触发条件,运行时周期性求值,脚本修改后自动重载无需重启 涌现出的高级行为 子 Agent 并行分发 :父 Agent 通过 code_run 启动自己的另一个实例,传入不同的 task-dir 。父子是同构进程——不存在特权的子 Agent 运行时对象,双方遵循完全相同的文件协议。上下文天然隔离(独立进程 = 独立内存空间),不需要复杂的状态管理来区分"哪些历史属于哪个子任务"。 看门狗监控 :反射模式 + 一个检测环境变化的触发脚本。当脚本返回非空字符串时,该字符串被作为任务分发到标准流水线。不需要专门的监控框架。 定时任务调度 :反射模式 + 一个检查时间条件的触发脚本。跟看门狗共享完全相同的底层机制,区别仅在于脚本内容。 自主空闲行为 :反射模式 + 一个"当前无任务"的触发条件。Agent 在空闲时自动执行预设的探索或维护任务。 为什么这能工作 这里的"涌现"不是物理学意义上的不可预测——而是工程意义上的: 当基础组件足够通用且组合成本足够低时,设计者未预先规划的功能可以在需要时被轻松实现。 关键不在于"意外",而在于"低成本"。传统框架每加一个高级功能需要修改核心代码、添加新模块、更新文档。GA 只需要写一个触发脚本或一段 code_run 调用——核心代码一行不改。 作者给出的数据:GA 核心代码 3300 行( Agent Loop 仅 92 行),而实现了子 Agent 并行、看门狗、定时调度、自主行为等全部高级功能。作为对比,OpenClaw 的代码量约 530,000 行——160 倍以上。这不是说代码少就一定好,但它说明了一件事: 当原语选对了,复杂行为不需要复杂实现。 7. Benchmark 数据 作者在 WebArena 、OSWorld 等标准 benchmark 上做了评测(数据来源:论文 Table 5 ): 指标 GA 总 Token 消耗 188,829 任务成功率( TSR ) 66.48% Token 消耗对比(同一组任务): 系统 Token 消耗 相对 GA GA 188,829 1x Claude Code 538,207 2.85x OpenClaw 633,498 3.35x 完整提示词长度对比(安装 20 个技能后,对 "Hello" 的响应): 系统 Full Prompt Length ( token ) OpenClaw 43,321 CodeX 23,932 Claude Code 22,821 GA 2,298 8. 从 Prompt Engineering 到 Context Engineering 作者提了一个我觉得很有价值的视角转变: Prompt Engineering 优化的是"一句话怎么说" Context Engineering 优化的是"每一轮对话中,模型看到的所有信息应该是什么" 在 Agent 场景下,上下文不只是用户指令,还包含工具定义、历史对话、记忆内容、工具返回值等多种组件。如何系统性地管理这些组件的注入、压缩和替换,才是决定 Agent 长期表现的关键。 GA 通过工具层、记忆层、压缩层和进化层四个维度,把 Context Engineering 落实为具体的系统机制。这不是一个 prompt 写得好不好的问题,而是一个系统架构问题。 9. 我的使用体感和局限 用了一段时间后的观察: 上下文 30K 的硬限制是双刃剑。 好处是 Agent 不会因为对话太长而变蠢;代价是单轮无法处理超大文件,需要分段读取。 记忆系统完全基于文件,没有向量检索。 SOP 数量特别多时,L1 索引的命中率可能下降。但对于个人使用场景(几十个 SOP ),目前没遇到问题。 code_run 是万能的,也是危险的。 能执行任意代码意味着安全性完全依赖部署环境的隔离。 SOP 质量很重要。 写得好的 SOP 能让 Agent 一步到位;写得不好的会误导。这是一个需要用户投入的地方。 多 Agent 协作通过进程 + 文件实现,没有结构化通信协议。 简单场景够用,复杂协作场景调试不太方便。 10. 总结 GA 的核心贡献不是某个具体的 trick ,而是一套完整的设计哲学: 在完备性与简洁性的结构性张力下,通过四层机制系统性地最大化上下文信息密度。 它证明了一件事:Agent 的能力上限不取决于上下文能塞多少字,而取决于在有限 token 预算里,能装进多少真正对当前决策有用的信息。 如果你正在做 Agent 开发,不管用不用 GA ,它的设计思路都值得参考——特别是"信息密度"这个视角,对 prompt 设计、记忆系统设计、工具集设计都有直接的指导意义。 项目地址: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 写在最后 在上一个帖子发出后,受到了广泛的关注,深感荣幸,所以火速加更! 贴几条热心的 v 友对我善意的人身攻击,我深刻的认识到了我的不足,并意识到自己 too young too simple ,sometimes naive 。 我做出如下承诺: 1 、今后 只写提示词,不写文章。 2 、在提示词中要求文章内容 去学术化,去叽里咕噜化,去指标化。 3 、更广泛的听取大家的批评,了解 V2EX 的社区规范,学习落实 v 站大佬的悉心教导。保证做到: “余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。故余虽愚,卒获有所闻。” 另外再回复一下这条: 本人再次重申,本人为某 top3 高校在读博士,大模型方向,于 3 月中旬刚刚恢复单身,欢迎感兴趣的异性 v 友留下联系方式,谢谢! 最后附上上面这篇文章的提示词: 欢迎大家用 ga 写点公众号文章或者软文吹 claude code 和 codex ,给自己挣点外快!
写在开头 FBI Warning⚠️:如果您没有使用过 ai 工具,没有相关的编程经验,或是对这个话题不敢兴趣,请您现在就退出当前页面。它将浪费你人生中宝贵的三分钟 友情提示:如果你想设计一个自己的 agent 或者想要深入理解 agent 如何高效运行,那么花 10 分钟理解本文会是你今年迄今为止对自己的时间做出的最值得的投资 想象一个项目工程,是做加法容易?还是减法容易?做一个通用 agent ,如何兼顾所有用户需求?如何能在简洁的前提下让一个有智慧的 agent 充分自举? GitHub: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 1. 核心问题:Agent 为什么跑着跑着就变蠢了? 做过 Agent 开发的应该都遇到过这个现象:Agent 在前几轮表现不错,但随着对话轮次增加,它开始丢约束、忘指令、重复犯错。 作者把这个问题归结为两个根本挑战: 挑战一:上下文爆炸。 每一轮交互都在往上下文里塞东西——工具定义、历史对话、工具返回值、检索到的记忆。这些内容在产生时各有用途,但对"下一步该做什么"的贡献参差不齐。无关内容不是被浪费那么简单,它会主动稀释模型的注意力,导致约束遗漏和幻觉。 挑战二:经验停滞。 如果 Agent 无法把成功经验沉淀下来,每次遇到类似任务都得从头探索。token 花了一堆,能力纹丝不动。作者管这叫 Stagnation Loop 。 这两个挑战的交汇点指向一个核心问题: LLM 的上下文到底应该塞什么? 2. 第一性原理:上下文信息密度最大化 GA 给出的答案是一个形式化的设计目标: D(C) = 决策相关信息量(C) / 上下文总长度(C) → max 翻译成人话:不追求上下文的长度,追求每一个 token 对当前决策的贡献密度。 这个目标拆开来看有两个维度: 完备性( Completeness ) :当前决策需要的信息必须显式出现在上下文中,不能让模型靠猜。 简洁性( Conciseness ) :无关和冗余的信息必须清除,让注意力聚焦在关键信号上。 作者提出了一个关键洞察: 完备性和简洁性之间的张力是结构性的,不是资源问题。 即使上下文窗口无限大,这个矛盾依然存在——因为加入更多"可能相关"的信息提升了完备性,却必然稀释注意力(削弱简洁性);而压缩提升了简洁性,却有丢失关键细节的风险(削弱完备性)。 所以 GA 的所有设计决策,本质上都是在这个结构性张力下做约束优化。 3. 为什么"上下文越长表现越差"——三重陷阱 这不是 GA 自己编的结论,是多篇论文验证过的现象。作者总结了三重相互强化的失效模式: 位置偏差( Lost-in-the-Middle ) :LLM 对上下文开头和结尾的信息利用率高,中间部分容易被"遗忘"。关键信息落在中间位置时,模型可能直接忽略。 注意力稀释( Attention Dilution ) :注意力是有限资源。无关内容越多,分配给每条关键信息的注意力越少。无关内容不是被浪费,而是主动干扰。 有效窗口远小于名义窗口 :一个标称 128K 的模型,真正能稳定推理的有效窗口可能只有几万 token 。随着上下文增长,推理能力逐渐退化。 这三者形成恶性循环:上下文膨胀 → 注意力稀释 → 位置偏差加剧 → 有效窗口收缩 → 系统倾向于注入更多"可能有用"的内容来补偿 → 上下文进一步膨胀。 核心启示:超过某个临界点后,增加更多上下文不仅无法提升性能,反而会降低表现。 4. GA 的系统性解法:四层信息密度优化 GA 不是靠一个 trick 解决问题,而是在信息生命周期的四个阶段分别做优化: 4.1 最小原子工具集——减少"先天噪声" 工具定义是上下文中 每轮都要重复支付的固定成本 。GA 的策略是极端克制:只保留 9 个原子工具。 为什么不是越多越好?作者指出工具膨胀有两层代价: Prompt 层 :每增加一个工具,就要在上下文中注入 Schema (名称、描述、参数类型)。53 个工具的 Schema 可能消耗上万 token ,而且每轮重复。 Policy 层 :工具越多,动作空间越大,选择歧义越高。比如"读文件"这个操作,如果同时有 FileReadTool 、GrepTool 、BashTool(cat),模型需要理解三者的微妙差异才能正确选择。 GA 的 9 个工具覆盖五大能力类:文件操作( file_read / file_write / file_patch )、代码执行( code_run )、网页交互( web_scan / web_execute_js )、记忆管理( update_working_checkpoint / start_long_term_update )、人机协作( ask_user )。 关键设计: code_run 是万能逃生舱。任何 9 个工具覆盖不到的长尾需求,都可以通过写代码来实现。这意味着工具集不需要为每个边缘场景增加专用工具——保持了工具层的极简,同时不牺牲能力上限。 code_run 本质上是图灵完备的! 实际使用分布(论文数据):code_run 34.4%、file_read 31.2%、update_working_checkpoint 17.2%,三个工具覆盖了 82.8% 的调用。 4.2 分层按需记忆——只加载"当前需要的" 传统方案要么不保留历史(每次从零开始),要么全量追加(上下文爆炸)。GA 用了一个四层架构: 层级 定位 是否 always-on 典型大小 L1 索引层 目录卡片,告诉 Agent "有哪些知识可用" 是 ~200 token L2 事实层 环境事实、用户偏好、服务器信息 否,按需 file_read 数百~数千 token L3 SOP 层 标准操作流程、技能脚本 否,按需 file_read 每个 SOP 数百 token L4 原始日志 完整对话历史,用于追溯和审计 否,极少访问 无限增长 核心机制: L1 始终在上下文中(成本极低),L2-L4 只在需要时才被加载。 这就像图书馆——你不会把所有书搬到桌上才开始工作,而是先查目录( L1 ),再去书架取需要的那本( L2/L3 )。 作者的消融实验验证了这个设计的有效性: 记忆配置 记忆大小( token ) 任务成功率 TSR No memory 0 52.44% Full memory (全量注入) 575 52.44% GA 分层记忆 165 66.48% 165 token 的分层记忆达到了 575 token 全量注入的 1.27 倍成功率。 全量注入反而跟没有记忆一样——因为无关信息稀释了注意力。这是"上下文越长表现越差"的直接实证。 4.3 上下文截断与压缩——主动瘦身 即使工具和记忆都控制住了,对话轮次增加后上下文仍会膨胀。GA 用四阶段压缩流水线处理: 工具返回值截断 :code_run 输出超长时只保留头尾 历史轮次压缩 :早期对话轮次被摘要化 消息驱逐 :超出预算的最旧消息被移除 工作记忆锚点注入 :通过 update_working_checkpoint 工具,Agent 主动把关键中间状态写入一个始终可见的锚点,防止被压缩丢失 第 4 点是个巧妙的设计——Agent 自己决定什么信息值得"钉住",而不是靠启发式规则猜测。 4.4 反思驱动的自我进化——让未来的上下文更精炼 GA 能把成功的任务经验蒸馏为 SOP 存入 L3 。下次遇到类似任务时,不需要在上下文中重新探索整个解决方案,直接调用之前积累的精炼经验。 这解决了"挑战二:经验停滞"。没有进化机制时,Agent 面临两难:要么重放更长的探索过程(削弱简洁性),要么从更短但信息不足的提示词开始(削弱完备性)。自我进化打破了这个两难——把冗长的探索轨迹压缩为紧凑的可复用知识。 5. 架构实现:92 行的 Agent Loop GA 的主循环只有约 92 行,结构是标准的 perceive-think-act: while not done: context = assemble(system_prompt, always_on_memory, tools, history) response = llm.chat(context) if response.has_tool_calls: results = execute(response.tool_calls) history.append(results) else: done = True 整个系统由 4 个文件构成: agent_loop.py (主循环)、 ga.py (工具实现)、 agentmain.py (入口 + 前端适配)、 llmcore.py ( LLM 调用封装)。总计约 3300 行。 这个极简架构的好处是: 没有隐藏的复杂度 。没有事件总线、没有调度守护进程、没有专用子 Agent 管理器。子 Agent 并行、定时任务、看门狗监控这些"高级功能",全部通过 9 个基础工具的组合涌现出来。 6. 约束下的涌现:三个原语长出整个生态 这是 GA 设计中我觉得最有意思的部分。 传统框架做高级功能的方式是"功能内置":需要子 Agent ?加一个 SubAgent Manager 。需要定时任务?加一个 Scheduler Daemon 。需要事件驱动?加一个 Event Bus 。每个新功能都带来新的接口、新的配置、新的故障模式。系统复杂度线性增长。 GA 走了另一条路: 不内置任何高级功能,只提供三个足够通用的原语,让高级行为从组合中涌现。 三个基础原语 原语 本质 一句话描述 自托管 CLI 入口点 Agent 就是一个命令行程序 任何能调用命令行的程序都能调用 GA——包括 GA 自己 文件协议 目录约定的跨进程通信 input.txt 送任务、output.txt 流结果、reply.txt 续对话、stop.txt 终止 反射模式 轮询 + 热重载 外部脚本定义触发条件,运行时周期性求值,脚本修改后自动重载无需重启 涌现出的高级行为 子 Agent 并行分发 :父 Agent 通过 code_run 启动自己的另一个实例,传入不同的 task-dir 。父子是同构进程——不存在特权的子 Agent 运行时对象,双方遵循完全相同的文件协议。上下文天然隔离(独立进程 = 独立内存空间),不需要复杂的状态管理来区分"哪些历史属于哪个子任务"。 看门狗监控 :反射模式 + 一个检测环境变化的触发脚本。当脚本返回非空字符串时,该字符串被作为任务分发到标准流水线。不需要专门的监控框架。 定时任务调度 :反射模式 + 一个检查时间条件的触发脚本。跟看门狗共享完全相同的底层机制,区别仅在于脚本内容。 自主空闲行为 :反射模式 + 一个"当前无任务"的触发条件。Agent 在空闲时自动执行预设的探索或维护任务。 为什么这能工作 这里的"涌现"不是物理学意义上的不可预测——而是工程意义上的: 当基础组件足够通用且组合成本足够低时,设计者未预先规划的功能可以在需要时被轻松实现。 关键不在于"意外",而在于"低成本"。传统框架每加一个高级功能需要修改核心代码、添加新模块、更新文档。GA 只需要写一个触发脚本或一段 code_run 调用——核心代码一行不改。 作者给出的数据:GA 核心代码 3300 行( Agent Loop 仅 92 行),而实现了子 Agent 并行、看门狗、定时调度、自主行为等全部高级功能。作为对比,OpenClaw 的代码量约 530,000 行——160 倍以上。这不是说代码少就一定好,但它说明了一件事: 当原语选对了,复杂行为不需要复杂实现。 7. Benchmark 数据 作者在 WebArena 、OSWorld 等标准 benchmark 上做了评测(数据来源:论文 Table 5 ): 指标 GA 总 Token 消耗 188,829 任务成功率( TSR ) 66.48% Token 消耗对比(同一组任务): 系统 Token 消耗 相对 GA GA 188,829 1x Claude Code 538,207 2.85x OpenClaw 633,498 3.35x 完整提示词长度对比(安装 20 个技能后,对 "Hello" 的响应): 系统 Full Prompt Length ( token ) OpenClaw 43,321 CodeX 23,932 Claude Code 22,821 GA 2,298 8. 从 Prompt Engineering 到 Context Engineering 作者提了一个我觉得很有价值的视角转变: Prompt Engineering 优化的是"一句话怎么说" Context Engineering 优化的是"每一轮对话中,模型看到的所有信息应该是什么" 在 Agent 场景下,上下文不只是用户指令,还包含工具定义、历史对话、记忆内容、工具返回值等多种组件。如何系统性地管理这些组件的注入、压缩和替换,才是决定 Agent 长期表现的关键。 GA 通过工具层、记忆层、压缩层和进化层四个维度,把 Context Engineering 落实为具体的系统机制。这不是一个 prompt 写得好不好的问题,而是一个系统架构问题。 9. 我的使用体感和局限 用了一段时间后的观察: 上下文 30K 的硬限制是双刃剑。 好处是 Agent 不会因为对话太长而变蠢;代价是单轮无法处理超大文件,需要分段读取。 记忆系统完全基于文件,没有向量检索。 SOP 数量特别多时,L1 索引的命中率可能下降。但对于个人使用场景(几十个 SOP ),目前没遇到问题。 code_run 是万能的,也是危险的。 能执行任意代码意味着安全性完全依赖部署环境的隔离。 SOP 质量很重要。 写得好的 SOP 能让 Agent 一步到位;写得不好的会误导。这是一个需要用户投入的地方。 多 Agent 协作通过进程 + 文件实现,没有结构化通信协议。 简单场景够用,复杂协作场景调试不太方便。 10. 总结 GA 的核心贡献不是某个具体的 trick ,而是一套完整的设计哲学: 在完备性与简洁性的结构性张力下,通过四层机制系统性地最大化上下文信息密度。 它证明了一件事:Agent 的能力上限不取决于上下文能塞多少字,而取决于在有限 token 预算里,能装进多少真正对当前决策有用的信息。 如果你正在做 Agent 开发,不管用不用 GA ,它的设计思路都值得参考——特别是"信息密度"这个视角,对 prompt 设计、记忆系统设计、工具集设计都有直接的指导意义。 项目地址: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 写在最后 在上一个帖子发出后,受到了广泛的关注,深感荣幸,所以火速加更! 贴几条热心的 v 友对我善意的人身攻击,我深刻的认识到了我的不足,并意识到自己 too young too simple ,sometimes naive 。 我做出如下承诺: 1 、今后 只写提示词,不写文章。 2 、在提示词中要求文章内容 去学术化,去叽里咕噜化,去指标化。 3 、更广泛的听取大家的批评,了解 V2EX 的社区规范,学习落实 v 站大佬的悉心教导。保证做到: “余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。故余虽愚,卒获有所闻。” 另外再回复一下这条: 本人再次重申,本人为某 top3 高校在读博士,大模型方向,于 3 月中旬刚刚恢复单身,欢迎感兴趣的异性 v 友留下联系方式,谢谢! 最后附上上面这篇文章的提示词: 欢迎大家用 ga 写点公众号文章或者软文吹 claude code 和 codex ,给自己挣点外快!
写在开头 FBI Warning⚠️:如果您没有使用过 ai 工具,没有相关的编程经验,或是对这个话题不敢兴趣,请您现在就退出当前页面。它将浪费你人生中宝贵的三分钟 友情提示:如果你想设计一个自己的 agent 或者想要深入理解 agent 如何高效运行,那么花 10 分钟理解本文会是你今年迄今为止对自己的时间做出的最值得的投资 想象一个项目工程,是做加法容易?还是减法容易?做一个通用 agent ,如何兼顾所有用户需求?如何能在简洁的前提下让一个有智慧的 agent 充分自举? GitHub: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 1. 核心问题:Agent 为什么跑着跑着就变蠢了? 做过 Agent 开发的应该都遇到过这个现象:Agent 在前几轮表现不错,但随着对话轮次增加,它开始丢约束、忘指令、重复犯错。 作者把这个问题归结为两个根本挑战: 挑战一:上下文爆炸。 每一轮交互都在往上下文里塞东西——工具定义、历史对话、工具返回值、检索到的记忆。这些内容在产生时各有用途,但对"下一步该做什么"的贡献参差不齐。无关内容不是被浪费那么简单,它会主动稀释模型的注意力,导致约束遗漏和幻觉。 挑战二:经验停滞。 如果 Agent 无法把成功经验沉淀下来,每次遇到类似任务都得从头探索。token 花了一堆,能力纹丝不动。作者管这叫 Stagnation Loop 。 这两个挑战的交汇点指向一个核心问题: LLM 的上下文到底应该塞什么? 2. 第一性原理:上下文信息密度最大化 GA 给出的答案是一个形式化的设计目标: D(C) = 决策相关信息量(C) / 上下文总长度(C) → max 翻译成人话:不追求上下文的长度,追求每一个 token 对当前决策的贡献密度。 这个目标拆开来看有两个维度: 完备性( Completeness ) :当前决策需要的信息必须显式出现在上下文中,不能让模型靠猜。 简洁性( Conciseness ) :无关和冗余的信息必须清除,让注意力聚焦在关键信号上。 作者提出了一个关键洞察: 完备性和简洁性之间的张力是结构性的,不是资源问题。 即使上下文窗口无限大,这个矛盾依然存在——因为加入更多"可能相关"的信息提升了完备性,却必然稀释注意力(削弱简洁性);而压缩提升了简洁性,却有丢失关键细节的风险(削弱完备性)。 所以 GA 的所有设计决策,本质上都是在这个结构性张力下做约束优化。 3. 为什么"上下文越长表现越差"——三重陷阱 这不是 GA 自己编的结论,是多篇论文验证过的现象。作者总结了三重相互强化的失效模式: 位置偏差( Lost-in-the-Middle ) :LLM 对上下文开头和结尾的信息利用率高,中间部分容易被"遗忘"。关键信息落在中间位置时,模型可能直接忽略。 注意力稀释( Attention Dilution ) :注意力是有限资源。无关内容越多,分配给每条关键信息的注意力越少。无关内容不是被浪费,而是主动干扰。 有效窗口远小于名义窗口 :一个标称 128K 的模型,真正能稳定推理的有效窗口可能只有几万 token 。随着上下文增长,推理能力逐渐退化。 这三者形成恶性循环:上下文膨胀 → 注意力稀释 → 位置偏差加剧 → 有效窗口收缩 → 系统倾向于注入更多"可能有用"的内容来补偿 → 上下文进一步膨胀。 核心启示:超过某个临界点后,增加更多上下文不仅无法提升性能,反而会降低表现。 4. GA 的系统性解法:四层信息密度优化 GA 不是靠一个 trick 解决问题,而是在信息生命周期的四个阶段分别做优化: 4.1 最小原子工具集——减少"先天噪声" 工具定义是上下文中 每轮都要重复支付的固定成本 。GA 的策略是极端克制:只保留 9 个原子工具。 为什么不是越多越好?作者指出工具膨胀有两层代价: Prompt 层 :每增加一个工具,就要在上下文中注入 Schema (名称、描述、参数类型)。53 个工具的 Schema 可能消耗上万 token ,而且每轮重复。 Policy 层 :工具越多,动作空间越大,选择歧义越高。比如"读文件"这个操作,如果同时有 FileReadTool 、GrepTool 、BashTool(cat),模型需要理解三者的微妙差异才能正确选择。 GA 的 9 个工具覆盖五大能力类:文件操作( file_read / file_write / file_patch )、代码执行( code_run )、网页交互( web_scan / web_execute_js )、记忆管理( update_working_checkpoint / start_long_term_update )、人机协作( ask_user )。 关键设计: code_run 是万能逃生舱。任何 9 个工具覆盖不到的长尾需求,都可以通过写代码来实现。这意味着工具集不需要为每个边缘场景增加专用工具——保持了工具层的极简,同时不牺牲能力上限。 code_run 本质上是图灵完备的! 实际使用分布(论文数据):code_run 34.4%、file_read 31.2%、update_working_checkpoint 17.2%,三个工具覆盖了 82.8% 的调用。 4.2 分层按需记忆——只加载"当前需要的" 传统方案要么不保留历史(每次从零开始),要么全量追加(上下文爆炸)。GA 用了一个四层架构: 层级 定位 是否 always-on 典型大小 L1 索引层 目录卡片,告诉 Agent "有哪些知识可用" 是 ~200 token L2 事实层 环境事实、用户偏好、服务器信息 否,按需 file_read 数百~数千 token L3 SOP 层 标准操作流程、技能脚本 否,按需 file_read 每个 SOP 数百 token L4 原始日志 完整对话历史,用于追溯和审计 否,极少访问 无限增长 核心机制: L1 始终在上下文中(成本极低),L2-L4 只在需要时才被加载。 这就像图书馆——你不会把所有书搬到桌上才开始工作,而是先查目录( L1 ),再去书架取需要的那本( L2/L3 )。 作者的消融实验验证了这个设计的有效性: 记忆配置 记忆大小( token ) 任务成功率 TSR No memory 0 52.44% Full memory (全量注入) 575 52.44% GA 分层记忆 165 66.48% 165 token 的分层记忆达到了 575 token 全量注入的 1.27 倍成功率。 全量注入反而跟没有记忆一样——因为无关信息稀释了注意力。这是"上下文越长表现越差"的直接实证。 4.3 上下文截断与压缩——主动瘦身 即使工具和记忆都控制住了,对话轮次增加后上下文仍会膨胀。GA 用四阶段压缩流水线处理: 工具返回值截断 :code_run 输出超长时只保留头尾 历史轮次压缩 :早期对话轮次被摘要化 消息驱逐 :超出预算的最旧消息被移除 工作记忆锚点注入 :通过 update_working_checkpoint 工具,Agent 主动把关键中间状态写入一个始终可见的锚点,防止被压缩丢失 第 4 点是个巧妙的设计——Agent 自己决定什么信息值得"钉住",而不是靠启发式规则猜测。 4.4 反思驱动的自我进化——让未来的上下文更精炼 GA 能把成功的任务经验蒸馏为 SOP 存入 L3 。下次遇到类似任务时,不需要在上下文中重新探索整个解决方案,直接调用之前积累的精炼经验。 这解决了"挑战二:经验停滞"。没有进化机制时,Agent 面临两难:要么重放更长的探索过程(削弱简洁性),要么从更短但信息不足的提示词开始(削弱完备性)。自我进化打破了这个两难——把冗长的探索轨迹压缩为紧凑的可复用知识。 5. 架构实现:92 行的 Agent Loop GA 的主循环只有约 92 行,结构是标准的 perceive-think-act: while not done: context = assemble(system_prompt, always_on_memory, tools, history) response = llm.chat(context) if response.has_tool_calls: results = execute(response.tool_calls) history.append(results) else: done = True 整个系统由 4 个文件构成: agent_loop.py (主循环)、 ga.py (工具实现)、 agentmain.py (入口 + 前端适配)、 llmcore.py ( LLM 调用封装)。总计约 3300 行。 这个极简架构的好处是: 没有隐藏的复杂度 。没有事件总线、没有调度守护进程、没有专用子 Agent 管理器。子 Agent 并行、定时任务、看门狗监控这些"高级功能",全部通过 9 个基础工具的组合涌现出来。 6. 约束下的涌现:三个原语长出整个生态 这是 GA 设计中我觉得最有意思的部分。 传统框架做高级功能的方式是"功能内置":需要子 Agent ?加一个 SubAgent Manager 。需要定时任务?加一个 Scheduler Daemon 。需要事件驱动?加一个 Event Bus 。每个新功能都带来新的接口、新的配置、新的故障模式。系统复杂度线性增长。 GA 走了另一条路: 不内置任何高级功能,只提供三个足够通用的原语,让高级行为从组合中涌现。 三个基础原语 原语 本质 一句话描述 自托管 CLI 入口点 Agent 就是一个命令行程序 任何能调用命令行的程序都能调用 GA——包括 GA 自己 文件协议 目录约定的跨进程通信 input.txt 送任务、output.txt 流结果、reply.txt 续对话、stop.txt 终止 反射模式 轮询 + 热重载 外部脚本定义触发条件,运行时周期性求值,脚本修改后自动重载无需重启 涌现出的高级行为 子 Agent 并行分发 :父 Agent 通过 code_run 启动自己的另一个实例,传入不同的 task-dir 。父子是同构进程——不存在特权的子 Agent 运行时对象,双方遵循完全相同的文件协议。上下文天然隔离(独立进程 = 独立内存空间),不需要复杂的状态管理来区分"哪些历史属于哪个子任务"。 看门狗监控 :反射模式 + 一个检测环境变化的触发脚本。当脚本返回非空字符串时,该字符串被作为任务分发到标准流水线。不需要专门的监控框架。 定时任务调度 :反射模式 + 一个检查时间条件的触发脚本。跟看门狗共享完全相同的底层机制,区别仅在于脚本内容。 自主空闲行为 :反射模式 + 一个"当前无任务"的触发条件。Agent 在空闲时自动执行预设的探索或维护任务。 为什么这能工作 这里的"涌现"不是物理学意义上的不可预测——而是工程意义上的: 当基础组件足够通用且组合成本足够低时,设计者未预先规划的功能可以在需要时被轻松实现。 关键不在于"意外",而在于"低成本"。传统框架每加一个高级功能需要修改核心代码、添加新模块、更新文档。GA 只需要写一个触发脚本或一段 code_run 调用——核心代码一行不改。 作者给出的数据:GA 核心代码 3300 行( Agent Loop 仅 92 行),而实现了子 Agent 并行、看门狗、定时调度、自主行为等全部高级功能。作为对比,OpenClaw 的代码量约 530,000 行——160 倍以上。这不是说代码少就一定好,但它说明了一件事: 当原语选对了,复杂行为不需要复杂实现。 7. Benchmark 数据 作者在 WebArena 、OSWorld 等标准 benchmark 上做了评测(数据来源:论文 Table 5 ): 指标 GA 总 Token 消耗 188,829 任务成功率( TSR ) 66.48% Token 消耗对比(同一组任务): 系统 Token 消耗 相对 GA GA 188,829 1x Claude Code 538,207 2.85x OpenClaw 633,498 3.35x 完整提示词长度对比(安装 20 个技能后,对 "Hello" 的响应): 系统 Full Prompt Length ( token ) OpenClaw 43,321 CodeX 23,932 Claude Code 22,821 GA 2,298 8. 从 Prompt Engineering 到 Context Engineering 作者提了一个我觉得很有价值的视角转变: Prompt Engineering 优化的是"一句话怎么说" Context Engineering 优化的是"每一轮对话中,模型看到的所有信息应该是什么" 在 Agent 场景下,上下文不只是用户指令,还包含工具定义、历史对话、记忆内容、工具返回值等多种组件。如何系统性地管理这些组件的注入、压缩和替换,才是决定 Agent 长期表现的关键。 GA 通过工具层、记忆层、压缩层和进化层四个维度,把 Context Engineering 落实为具体的系统机制。这不是一个 prompt 写得好不好的问题,而是一个系统架构问题。 9. 我的使用体感和局限 用了一段时间后的观察: 上下文 30K 的硬限制是双刃剑。 好处是 Agent 不会因为对话太长而变蠢;代价是单轮无法处理超大文件,需要分段读取。 记忆系统完全基于文件,没有向量检索。 SOP 数量特别多时,L1 索引的命中率可能下降。但对于个人使用场景(几十个 SOP ),目前没遇到问题。 code_run 是万能的,也是危险的。 能执行任意代码意味着安全性完全依赖部署环境的隔离。 SOP 质量很重要。 写得好的 SOP 能让 Agent 一步到位;写得不好的会误导。这是一个需要用户投入的地方。 多 Agent 协作通过进程 + 文件实现,没有结构化通信协议。 简单场景够用,复杂协作场景调试不太方便。 10. 总结 GA 的核心贡献不是某个具体的 trick ,而是一套完整的设计哲学: 在完备性与简洁性的结构性张力下,通过四层机制系统性地最大化上下文信息密度。 它证明了一件事:Agent 的能力上限不取决于上下文能塞多少字,而取决于在有限 token 预算里,能装进多少真正对当前决策有用的信息。 如果你正在做 Agent 开发,不管用不用 GA ,它的设计思路都值得参考——特别是"信息密度"这个视角,对 prompt 设计、记忆系统设计、工具集设计都有直接的指导意义。 项目地址: https://github.com/juntao-ai/GenericAgent 论文: https://arxiv.org/pdf/2604.17091 教程: https://datawhalechina.github.io/hello-generic-agent/ 写在最后 在上一个帖子发出后,受到了广泛的关注,深感荣幸,所以火速加更! 贴几条热心的 v 友对我善意的人身攻击,我深刻的认识到了我的不足,并意识到自己 too young too simple ,sometimes naive 。 我做出如下承诺: 1 、今后 只写提示词,不写文章。 2 、在提示词中要求文章内容 去学术化,去叽里咕噜化,去指标化。 3 、更广泛的听取大家的批评,了解 V2EX 的社区规范,学习落实 v 站大佬的悉心教导。保证做到: “余立侍左右,援疑质理,俯身倾耳以请;或遇其叱咄,色愈恭,礼愈至,不敢出一言以复;俟其欣悦,则又请焉。故余虽愚,卒获有所闻。” 另外再回复一下这条: 本人再次重申,本人为某 top3 高校在读博士,大模型方向,于 3 月中旬刚刚恢复单身,欢迎感兴趣的异性 v 友留下联系方式,谢谢! 最后附上上面这篇文章的提示词: 欢迎大家用 ga 写点公众号文章或者软文吹 claude code 和 codex ,给自己挣点外快!