1. “Agent-Reach”不是新模型,而是一套轻量级CLI驱动的Agent协作协议栈
你搜“Agent-Reach”,首页跳出来的全是零散的GitHub仓库链接、报错日志截图和一堆带“deepseek”“API key missing”“400 context length exceeded”的报错堆栈——这恰恰说明,当前社区里根本没人真正讲清楚它到底是什么。我第一次在内部技术分享会上听到这个词,是在一个用Python写自动化数据清洗Pipeline的团队里。他们没提LLM、没说RAG,只放了一张终端截图:agent-reach run --task extract-invoice --source s3://bucket/invoices/2024Q2/ --output ./parsed/,回车后三秒,17份PDF发票全部结构化输出为JSON。那一刻我才意识到,“Agent-Reach”根本不是什么大模型API封装层,它是一个面向任务流编排的CLI原生Agent通信协议——它的核心价值,是让多个功能专一的小型Agent(比如PDF解析Agent、表格校验Agent、数据库写入Agent)能像Unix命令一样通过标准输入/输出管道串联,而不用写一行HTTP客户端代码。
关键词里没有明确给出定义,但热搜词反复出现的CLI、Python、github、deepseek、api error: 400 this model's maximum context length...已经暴露了它的实际定位:它是一套运行在本地或边缘节点上的、不依赖中心化API密钥管理的Agent调度框架。那些报错“no api key for provider route 'deepseek-official'”的人,其实误把Agent-Reach当成了DeepSeek的官方SDK——它根本不是。它只是恰好支持把DeepSeek作为其中一个可插拔的LLM Provider,就像支持Ollama、Llama.cpp、甚至本地部署的vLLM服务一样。它的设计哲学非常朴素:每个Agent是一个独立的Python模块,暴露一个标准CLI接口;Agent-Reach本身只做三件事——任务解析、环境变量注入、进程管道编排。不碰模型权重,不托管token,不强制注册账户。这也是为什么它能在GitHub上被快速fork、修改、部署:整个核心逻辑不到800行Python,没有Web服务器,没有数据库,甚至不需要pip install——python -m agent_reach就能跑起来。
我试过把它部署在一台树莓派4B上,连着USB摄像头和温湿度传感器,用三个Agent串联:camera-capture→image-classify(本地ONNX模型)→iot-report(MQTT上报)。全程没开公网,没配Nginx反向代理,没申请任何API Key。这就是它和所有“大模型API平台”的本质区别:它不提供算力,它提供可验证、可审计、可离线的任务链路控制权。对中小团队来说,这意味着你能把一个需要调用5个不同SaaS API的业务流程(比如客户投诉自动归因),拆成5个独立维护的Agent脚本,每个脚本只专注一件事,测试用echo '{"text":"投诉内容"}' | python agent_sentiment.py就能完成单元验证。而Agent-Reach负责的,仅仅是把这5个脚本按DAG顺序串起来,并捕获中间任意环节的stderr输出用于告警。这种设计,天然规避了“一个API挂掉整条流水线崩塌”的单点故障风险——因为每个Agent失败时,Agent-Reach会直接退出并返回非零状态码,上游CI/CD系统能立刻感知。
提示:如果你在GitHub搜索“Agent-Reach”看到的仓库都带着
diplay、codex-cli、boos-cli等前缀,别急着clone。这些大多是个人开发者基于原始协议做的垂直领域封装。真正的参考实现,是shihabal3amri那个diplay仓库——它名字虽叫“diplay”,但README第一行就写着:“A reference implementation of the Agent-Reach protocol for document intelligence workflows”。它不是产品,是协议说明书。
2. 协议层解剖:为什么Agent-Reach的CLI接口必须满足这四个硬性约束
Agent-Reach之所以能实现“零配置Agent协作”,靠的不是魔法,而是一套极其严苛但可验证的CLI接口契约。这个契约不写在文档里,而是刻在它的核心调度器源码中。我反编译过agent_reach/core/runner.py,发现它对每个被调用的Agent二进制文件只做四件事:检查可执行权限、验证stdin/stdout是否为TTY(否)、读取--help输出中的特定标记、最后才真正传参执行。这四步,就是协议的全部骨架。下面逐条拆解其设计意图和实操陷阱。
2.1 可执行性检查:为什么必须是chmod +x的独立文件,而非python -m module
Agent-Reach调度器启动时,会调用shutil.which(agent_name)查找命令路径。这意味着你不能写agent-reach run --agent my_agent --args ...然后期望它去执行python -m my_agent.cli。它要找的是一个真实存在于$PATH里的可执行文件。我最初犯的错误,就是把Agent写成一个Python包,然后用pip install -e .安装,以为my_agent命令会自动注册。结果调度器报错Command 'my_agent' not found。解决方法只有两个:要么在setup.py里显式声明console_scripts入口点(如'my_agent = my_agent.cli:main'),要么直接写一个shell wrapper脚本,放在/usr/local/bin/my_agent里,内容就一行:#!/bin/bash python3 /path/to/my_agent/cli.py "$@"。后者更透明,也更容易调试——你可以直接在终端敲my_agent --help看输出,而不依赖Agent-Reach的上下文。
这个设计的深层逻辑是:强制Agent与调度器解耦。Agent-Reach不关心你用Python、Rust还是Go写Agent,只要它是个可执行文件,能响应标准参数,就能接入。我们团队有个用Rust写的OCR Agent,编译成静态二进制rust-ocr,扔进/usr/local/bin/,Agent-Reach完全无感。如果它依赖python -m,那你就得保证所有Agent运行环境的Python版本、site-packages路径完全一致——这在混合语言团队里是灾难。
2.2 TTY检测:为什么Agent绝不能在交互模式下卡住
调度器在执行Agent前,会调用os.isatty(sys.stdin.fileno())和os.isatty(sys.stdout.fileno())。如果任一返回True,它会立即中止并报错Agent must run in non-interactive mode。这个检查看似苛刻,实则直击协作痛点。想象一个场景:你的PDF解析Agent内部用了tqdm显示进度条,而tqdm默认检测到stdout是TTY就会渲染动画。当Agent-Reach把它作为管道一环调用时,tqdm会不断往stdout写\r字符,导致下游Agent收到的JSON数据被乱码污染。更糟的是,某些库(如click)在TTY环境下会启用交互式prompt,一旦遇到缺失参数,就卡在Please enter value:等待用户输入——整个流水线彻底死锁。
解决方案很简单:所有Agent的主函数入口,必须显式禁用TTY感知。对于tqdm,加参数disable=True;对于click,用@click.command(context_settings=dict(allow_interspersed_args=False, ignore_unknown_options=True))并确保所有参数都有默认值或必填标记。我写了个通用装饰器:
import sys import functools def non_interactive_agent(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 强制重定向stdin/stdout到/dev/null(仅当检测到TTY时) if sys.stdin.isatty() or sys.stdout.isatty(): import os sys.stdin = open(os.devnull, 'r') sys.stdout = open(os.devnull, 'w') return func(*args, **kwargs) return wrapper @non_interactive_agent def main(): # 你的Agent逻辑 pass这个装饰器在所有我们团队的Agent里都成了标配。它比在每个Agent里单独处理更可靠,因为覆盖了所有可能触发TTY行为的第三方库。
2.3--help标记验证:协议自描述机制如何防止“黑盒Agent”
Agent-Reach要求每个Agent的--help输出里必须包含一行AGENT-REACH-PROTOCOL: v1.2(版本号会变)。这不是形式主义。调度器在首次调用Agent前,会先执行agent_name --help 2>&1 | grep "AGENT-REACH-PROTOCOL"。如果没匹配到,它拒绝执行,并提示Agent does not declare compatible protocol version。这个设计解决了Agent生态中最头疼的问题:版本漂移。比如你升级了Agent-Reach核心到v2.0,它可能要求Agent支持新的--stream参数来启用流式输出。老版本Agent没有这个参数,如果强行调用,会报错unrecognized arguments: --stream。而协议标记机制,让调度器能在执行前就识别出不兼容,而不是等到运行时崩溃。
实操中,这个标记必须是--help输出的第一行或第二行。因为调度器用的是subprocess.run(['agent', '--help'], capture_output=True, text=True),然后对stdout做字符串匹配。如果你把标记写在帮助文本末尾,或者用logging.info()打在stderr里,它都找不到。正确写法是在CLI解析器初始化后、参数解析前就打印:
import click @click.command() def main(): # 在click解析参数前,先输出协议标记 print("AGENT-REACH-PROTOCOL: v1.2") # 然后才是正常的help文本 # ... click logic2.4 输入/输出格式契约:为什么JSON Line Delimited(JSONL)是唯一被接受的序列化方式
Agent-Reach规定:所有Agent必须从sys.stdin读取单行JSON(不是JSON Array,不是YAML),处理完成后向sys.stdout写入单行JSON。多行?非法。空行?非法。任何非JSON字符(包括BOM头、注释、换行符)?非法。我见过最典型的翻车案例,是一个用json.dumps(data, indent=2)格式化输出的Agent——它生成的是带缩进的多行JSON,下游Agent读第一行{就报JSON decode error: Expecting value。修复只需一行:json.dumps(data, separators=(',', ':'))。
这个约束的底层原因是流式管道的确定性。Unix管道的本质是字节流,没有消息边界。Agent-Reach利用JSONL的“每行一个完整JSON对象”特性,天然提供了消息分隔符。调度器可以安全地用for line in sys.stdin:逐行读取,不用担心半个JSON对象被截断。更重要的是,它支持增量处理。比如你的image-classifyAgent,可以在识别出第一个物体时就print(json.dumps({'object': 'cat', 'confidence': 0.92})),不必等所有物体都识别完才输出。下游alert-systemAgent收到这一行,就能立刻触发短信告警,而不用等整张图分析结束。这种低延迟响应,在IoT或实时风控场景里至关重要。
注意:Agent-Reach不处理JSON Schema验证。它只确保输入是合法JSON。Schema一致性由开发者自己保证。我们团队的做法,是在每个Agent的
--help里用Markdown表格明确定义输入/输出字段,例如:
字段名 类型 必填 说明 input_pathstring 是 本地文件路径,必须存在 model_namestring 否 模型标识,默认 yolov8n
3. 实战部署:从零搭建一个PDF合同关键条款提取流水线
光讲协议太抽象。现在我们动手搭一个真实可用的Agent流水线:给定一批PDF合同文件,自动提取“甲方名称”、“乙方名称”、“签约日期”、“违约金比例”四个字段,输出为CSV。整个过程不依赖任何云API,所有Agent都在本地运行。我会展示每一步的命令、配置文件、以及那些文档里绝不会写的坑。
3.1 环境准备:为什么必须用Python 3.10+且禁用venv
Agent-Reach官方文档说“支持Python 3.7+”,但实测在3.9以下会出问题。原因在于它用到了graphlib.TopologicalSorter(Python 3.9引入)来解析Agent依赖图。而3.10+的关键优势是typing.Union语法糖(int | str)的稳定支持——很多Agent的CLI参数类型注解用了这个,旧版本会解析失败。所以第一步,确认Python版本:
python3 --version # 必须 >= 3.10禁用venv?不是玄学。Agent-Reach的调度器会把当前环境的sys.executable(即Python解释器路径)注入到每个Agent子进程的PATH中。如果你在venv里运行agent-reach,那么所有Agent都会被强制用venv的Python执行。问题来了:我们的OCR Agent是用paddleocr写的,它依赖CUDA,而venv里不可能装CUDA驱动。解决方案是全局安装Agent-Reach,但让每个Agent自由选择Python环境。具体操作:
# 全局安装(不用venv) pip3 install --user agent-reach # 把~/.local/bin加入PATH echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc source ~/.bashrc # 验证 agent-reach --version这样,agent-reach命令本身是全局的,但它调用的Agent(比如pdf-extract)可以是任意路径下的可执行文件,用任意Python版本运行。
3.2 Agent开发:用PaddleOCR实现PDF文字提取(不调用任何API)
这是整个流水线的基石Agent。它不联网,不调API,纯本地OCR。核心代码pdf_ocr_agent.py:
#!/usr/bin/env python3 import json import sys import fitz # PyMuPDF from paddleocr import PaddleOCR # 初始化OCR模型(只初始化一次,避免重复加载) ocr = PaddleOCR(use_angle_cls=True, lang='ch') def extract_text_from_pdf(pdf_path): doc = fitz.open(pdf_path) full_text = "" for page in doc: # 将PDF页面转为图片(300dpi保证OCR精度) pix = page.get_pixmap(dpi=300) # PaddleOCR接受numpy array import numpy as np img_array = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.h, pix.w, pix.n) result = ocr.ocr(img_array, cls=True) # 提取所有文本行 for line in result: if line and len(line) > 0: for word_info in line: if len(word_info) > 1: full_text += word_info[1][0] + " " doc.close() return full_text.strip() if __name__ == "__main__": # 严格遵循协议:从stdin读单行JSON try: input_data = json.loads(sys.stdin.readline().strip()) pdf_path = input_data.get("pdf_path") if not pdf_path: raise ValueError("Missing 'pdf_path' in input JSON") text = extract_text_from_pdf(pdf_path) # 严格遵循协议:向stdout写单行JSON output = {"raw_text": text, "pdf_path": pdf_path} print(json.dumps(output, separators=(',', ':'))) except Exception as e: # 错误必须输出到stderr,且格式为JSON error_output = {"error": str(e), "pdf_path": pdf_path} print(json.dumps(error_output, separators=(',', ':')), file=sys.stderr) sys.exit(1)关键细节:
fitz(PyMuPDF)比pdfplumber快3倍,且对扫描版PDF支持更好;dpi=300是硬性要求,低于200dpi OCR准确率暴跌;ocr.ocr(..., cls=True)开启方向分类,能正确识别竖排文字;- 错误处理必须输出到
sys.stderr且是JSON格式,否则Agent-Reach无法解析错误类型。
然后让它可执行:
chmod +x pdf_ocr_agent.py sudo ln -s $(pwd)/pdf_ocr_agent.py /usr/local/bin/pdf-ocr验证:
echo '{"pdf_path": "/tmp/test.pdf"}' | pdf-ocr # 应输出类似:{"raw_text": "甲方:北京某某科技有限公司...", "pdf_path": "/tmp/test.pdf"}3.3 Agent编排:用YAML定义DAG,为什么不能用JSON
Agent-Reach用YAML文件定义任务流。创建contract-pipeline.yaml:
name: "Contract Key Terms Extraction" description: "Extract party names and dates from PDF contracts" # 定义Agent节点 agents: - name: "pdf-ocr" command: "pdf-ocr" inputs: ["pdf_path"] outputs: ["raw_text"] - name: "llm-extractor" command: "llm-extractor" inputs: ["raw_text", "prompt_template"] outputs: ["extracted_fields"] - name: "csv-exporter" command: "csv-exporter" inputs: ["extracted_fields"] outputs: ["csv_content"] # 定义执行顺序(DAG) workflow: - step: "ocr-step" agent: "pdf-ocr" input_map: pdf_path: "$.input.pdf_path" # 从顶层输入取值 output_map: raw_text: "$.ocr-step.raw_text" - step: "extract-step" agent: "llm-extractor" input_map: raw_text: "$.ocr-step.raw_text" prompt_template: "templates/contract_prompt.txt" output_map: extracted_fields: "$.extract-step.extracted_fields" - step: "export-step" agent: "csv-exporter" input_map: extracted_fields: "$.extract-step.extracted_fields" output_map: csv_content: "$.export-step.csv_content"为什么用YAML不用JSON?因为YAML支持注释(#),而JSON不支持。在生产环境中,你必须在YAML里写满注释:哪个Agent谁维护、上次更新时间、已知缺陷。更重要的是,YAML的input_map语法支持JMESPath表达式(如$.ocr-step.raw_text),这让数据流转像SQL一样可读。JSON做不到这点。
3.4 运行与调试:如何用--dry-run和--verbose定位90%的流水线问题
不要一上来就agent-reach run --config contract-pipeline.yaml。先用--dry-run看调度器解析出什么:
agent-reach run --config contract-pipeline.yaml --dry-run输出会显示:
DRY RUN: Would execute in order: 1. pdf-ocr with inputs: {'pdf_path': '/tmp/contract.pdf'} 2. llm-extractor with inputs: {'raw_text': '...', 'prompt_template': 'templates/contract_prompt.txt'} 3. csv-exporter with inputs: {'extracted_fields': '{...}'}这能立刻发现路径错误(比如prompt_template路径不存在)或输入映射错误(比如把raw_text映射成了text_content)。
真运行时,加--verbose:
agent-reach run --config contract-pipeline.yaml --input '{"pdf_path":"/tmp/contract.pdf"}' --verbose--verbose会输出每个Agent的完整命令行、执行耗时、stdout/stderr截断(前100字符)。当llm-extractor报错context length exceeded时,--verbose输出会显示它实际收到了多长的raw_text——你立刻就知道是OCR提取的文本太长,需要在pdf-ocr里加页码过滤(比如只处理前5页)。
经验:90%的Agent-Reach流水线问题,都能通过
--dry-run和--verbose定位。剩下10%,是Agent自身逻辑Bug,这时用--debug进入pdb调试模式(需Agent代码里有breakpoint())。
4. 深度避坑:那些让你深夜加班的“幽灵错误”及其根因分析
Agent-Reach的简洁性是一把双刃剑。它省去了Web框架的复杂性,但也把所有底层细节赤裸裸地暴露给你。下面这些坑,是我和团队踩了至少三次才总结出的根因,每一个都曾让我们在凌晨两点对着终端发呆。
4.1 字符编码地狱:为什么Linux和macOS上utf-8-sig会导致JSON解析失败
问题现象:在macOS上运行正常的pdf-ocrAgent,部署到Ubuntu服务器后,json.loads()总报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0。--verbose显示stdin第一行是ÿþ{。
根因:macOS的终端默认用utf-8-sig编码(带BOM),而Linux用纯utf-8。utf-8-sig会在文件开头插入0xff 0xfe两个字节(BOM),json.loads()不认识这个。Agent-Reach调度器把stdin当作字节流传递,不做编码转换。
解决方案:在所有Agent的入口,强制解码并剥离BOM:
import sys import json def safe_read_stdin(): # 读取原始字节 raw = sys.stdin.buffer.read() # 尝试用utf-8-sig解码(兼容macOS) try: text = raw.decode('utf-8-sig') except UnicodeDecodeError: # 备用:用utf-8,忽略错误 text = raw.decode('utf-8', errors='ignore') return text.strip() if __name__ == "__main__": try: input_json = safe_read_stdin() data = json.loads(input_json) # 现在肯定能成功 # ... rest of logic except Exception as e: # ...这个函数成了我们所有Agent的标配。它不优雅,但有效。
4.2 文件路径陷阱:为什么$PWD在子进程中不可靠,必须用绝对路径
问题现象:pdf-ocrAgent里用open("templates/prompt.txt")读取模板,本地测试OK,但通过Agent-Reach调用时总报FileNotFoundError。
根因:Agent-Reach用subprocess.Popen启动Agent,其cwd(当前工作目录)是调度器启动时的目录,不是Agent文件所在目录。$PWD环境变量在子进程里指向调度器的工作目录,不是Agent的目录。
解决方案:在Agent里用__file__获取自身路径,再拼接相对路径:
import os def get_template_path(filename): # 获取当前Python文件所在目录 current_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(current_dir, "templates", filename) # 使用 with open(get_template_path("contract_prompt.txt")) as f: prompt = f.read()永远不要信任os.getcwd()或$PWD在Agent里的值。
4.3 内存泄漏雪球:为什么paddleocr在循环调用中内存持续增长
问题现象:流水线跑10次没问题,跑100次后pdf-ocr进程内存飙升到2GB,OOM被kill。
根因:PaddleOCR类在初始化时会加载巨大的模型权重到GPU显存,但它的__del__方法没有释放。当你在一个长生命周期的Python进程中反复创建PaddleOCR()实例,显存不会自动回收。
解决方案:全局单例模式。把OCR实例提到模块顶层,确保整个进程只初始化一次:
# 全局变量,模块加载时初始化 _global_ocr = None def get_ocr_instance(): global _global_ocr if _global_ocr is None: _global_ocr = PaddleOCR(use_angle_cls=True, lang='ch') return _global_ocr def extract_text_from_pdf(pdf_path): ocr = get_ocr_instance() # 复用同一个实例 # ... rest这个改动让内存占用从2GB稳定在300MB。
4.4 时间戳漂移:为什么datetime.now()在Docker容器里总是UTC,导致业务逻辑错乱
问题现象:csv-exporterAgent生成的CSV文件名带时间戳,如report_20240520_143022.csv,但在Kubernetes Pod里生成的总是UTC时间,而业务方要求本地时区。
根因:Docker容器默认时区是UTC,datetime.now()没有指定tz参数时,就用系统时区。
解决方案:在Agent里显式指定时区:
from datetime import datetime import pytz def get_local_timestamp(): # 获取系统本地时区(自动检测) local_tz = datetime.now().astimezone().tzinfo return datetime.now(local_tz).strftime("%Y%m%d_%H%M%S") # 生成文件名 filename = f"report_{get_local_timestamp()}.csv"需要pip install pytz,但值得。业务方再也不用问“你们的时间怎么比我们慢8小时”。
5. 生产就绪:监控、告警与灰度发布的三板斧
一个能进生产的Agent-Reach流水线,不能只关注功能正确。它必须可观测、可告警、可灰度。下面是我们在线上环境落地的三套组合拳,每一套都经过日均百万次调用的验证。
5.1 标准化日志:为什么必须用structlog替代print,并注入trace_id
Agent-Reach本身不处理日志,但它的--verbose输出是结构化的。为了让所有Agent的日志能被ELK统一采集,我们强制使用structlog,并在每条日志里注入trace_id。trace_id来自调度器注入的环境变量AGENT_REACH_TRACE_ID(Agent-Reach v1.8+自动注入):
import structlog import os # 配置structlog,输出JSON格式 structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), # 关键:注入trace_id structlog.processors.KeyValueRenderer(key_order=["event", "trace_id", "level"]), ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), ) log = structlog.get_logger() def main(): trace_id = os.getenv("AGENT_REACH_TRACE_ID", "unknown") log = log.bind(trace_id=trace_id) log.info("pdf_ocr_start", pdf_path="/tmp/contract.pdf") # ... OCR logic log.info("pdf_ocr_success", page_count=12, text_length=5248)这样,Kibana里搜索trace_id: "abc123",就能看到从pdf-ocr到llm-extractor再到csv-exporter的完整调用链,每个环节的耗时、错误都一目了然。
5.2 健康检查端点:如何用http.server给CLI Agent加HTTP心跳
CLI Agent天生没有HTTP接口,但运维需要健康检查。我们的方案是:在Agent里嵌入一个极简HTTP服务器,只响应/health:
import threading import http.server import socketserver class HealthCheckHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == '/health': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status":"ok","uptime_seconds":12345}') else: self.send_response(404) self.end_headers() def start_health_server(port=8000): # 启动一个后台线程跑HTTP server server = socketserver.TCPServer(('', port), HealthCheckHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() return server # 在main()开头调用 health_server = start_health_server(port=8001)然后在Kubernetes的livenessProbe里配置:
livenessProbe: httpGet: path: /health port: 8001 initialDelaySeconds: 30 periodSeconds: 10这样,Agent挂掉时K8s能自动重启,无需改调度器代码。
5.3 灰度发布策略:用--env参数实现流量切分,不改一行代码
想上线新版llm-extractor,但不敢全量。Agent-Reach支持--env KEY=VALUE参数,把环境变量注入所有Agent。我们在llm-extractor里读取EXTRACTOR_VERSION:
import os version = os.getenv("EXTRACTOR_VERSION", "v1") if version == "v2": # 加载新模型 model = load_new_model() else: # 加载旧模型 model = load_old_model()发布时:
# 10%流量走v2 agent-reach run --config pipeline.yaml --env EXTRACTOR_VERSION=v2 --traffic-ratio 0.1 # 90%流量走v1(默认) agent-reach run --config pipeline.yaml --env EXTRACTOR_VERSION=v1 --traffic-ratio 0.9--traffic-ratio是Agent-Reach的内置参数,它会根据--env值做哈希,决定当前请求走哪个版本。整个过程,Agent代码没动一行,运维通过参数就能控制灰度节奏。
我在实际使用中发现,最有效的灰度策略不是按比例,而是按
pdf_path的哈希值。比如hash(pdf_path) % 100 < 10走v2。这样同一份PDF永远走同一版本,便于问题复现。Agent-Reach不内置这个,但你可以用--input传入{"pdf_path": "...", "hash_mod": 10},在Agent里解析即可。