14 Risk-Aware Fixed-Order Agent¶
这一份 notebook 专门比较 3 种 fixed-order questioning agent:
- 原始 fixed order:
age -> sex -> location - risk-aware candidate A:
age -> sex -> location - risk-aware candidate B:
age -> location -> sex
注意:candidate A 和原始 fixed order 顺序相同,所以这一轮里真正有差异的是:
- 原始 fixed order / candidate A
- candidate B
但我们仍然把三者都保留下来,方便论文里写清楚来源。
from pathlib import Path
from datetime import datetime
import ast
import json
import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score, balanced_accuracy_score, f1_score
PROJECT_ROOT = Path('/Users/applesues01/Documents/Medical_Agent')
SUPPORT_DIR = PROJECT_ROOT / 'supports'
MODEL_REGISTRY_PATH = SUPPORT_DIR / '2026-08-03_194704_all_saved_metadata_models.csv'
VALIDATED_AGENT_PATH = SUPPORT_DIR / '2026-08-03_185733_validated_agent_comparison.csv'
print(MODEL_REGISTRY_PATH)
print(VALIDATED_AGENT_PATH)
/Users/applesues01/Documents/Medical_Agent/supports/2026-08-03_194704_all_saved_metadata_models.csv /Users/applesues01/Documents/Medical_Agent/supports/2026-08-03_185733_validated_agent_comparison.csv
1. 先整理我们要比较的 3 个顺序¶
comparison_orders = [
{
'agent_name': 'original_fixed_order',
'question_order': ['age', 'sex', 'location'],
'source': 'advisor_baseline'
},
{
'agent_name': 'risk_aware_candidate_a',
'question_order': ['age', 'sex', 'location'],
'source': 'risk_aware_score_sorted'
},
{
'agent_name': 'risk_aware_candidate_b',
'question_order': ['age', 'location', 'sex'],
'source': 'risk_aware_conservative'
}
]
comparison_order_df = pd.DataFrame(comparison_orders)
comparison_order_df
| agent_name | question_order | source | |
|---|---|---|---|
| 0 | original_fixed_order | [age, sex, location] | advisor_baseline |
| 1 | risk_aware_candidate_a | [age, sex, location] | risk_aware_score_sorted |
| 2 | risk_aware_candidate_b | [age, location, sex] | risk_aware_conservative |
2. 读取之前的 baseline agent 结果¶
这里我们把以前已经跑出来的 validated baselines 一起读进来,方便后面对照。
validated_agent_df = pd.read_csv(VALIDATED_AGENT_PATH)
validated_agent_df
| agent_name | max_questions | avg_questions | expected_accuracy | expected_balanced_accuracy | expected_macro_f1 | threshold | |
|---|---|---|---|---|---|---|---|
| 0 | fixed_order | 2 | 2.0 | 0.793383 | 0.582281 | 0.581666 | NaN |
| 1 | uncertainty | 2 | 2.0 | 0.793383 | 0.582281 | 0.581666 | 0.605 |
| 2 | lookahead | 2 | 2.0 | 0.792708 | 0.598458 | 0.596497 | NaN |
3. 这一轮我们先做“实验设计确认”¶
这一份 notebook 的目标不是重写整套 agent 逻辑,而是明确:
- 哪些顺序需要比较
- 输出哪些指标
- 保存成什么格式
你已经把核心顺序跑出来了,所以这里先把对比框架立好。
planned_metrics = [
'avg_questions',
'expected_accuracy',
'expected_balanced_accuracy',
'expected_macro_f1'
]
planned_metrics
['avg_questions', 'expected_accuracy', 'expected_balanced_accuracy', 'expected_macro_f1']
4. 当前已知事实¶
由于 candidate_a 和原始 fixed order 完全同序,所以这一轮真正新的实验重点是:
risk_aware_candidate_b = age -> location -> sex
如果它优于原始 fixed order,说明:
在固定顺序设定下,把高风险问题往后放,可能带来更好的风险-性能平衡。
print('Original fixed order :', comparison_orders[0]['question_order'])
print('Risk-aware candidate A :', comparison_orders[1]['question_order'])
print('Risk-aware candidate B :', comparison_orders[2]['question_order'])
Original fixed order : ['age', 'sex', 'location'] Risk-aware candidate A : ['age', 'sex', 'location'] Risk-aware candidate B : ['age', 'location', 'sex']
5. 保存这一步的顺序配置¶
这一格先把本轮要比较的 fixed-order 配置保存下来,方便后面继续接实验。
timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
order_config_path = SUPPORT_DIR / f'{timestamp}_risk_aware_fixed_order_configs.csv'
comparison_order_df.to_csv(order_config_path, index=False)
print(order_config_path)
/Users/applesues01/Documents/Medical_Agent/supports/2026-08-04_164518_risk_aware_fixed_order_configs.csv
6. 下一步真正要做什么¶
下一步我们就要把 candidate_b 接入你之前的 validated fixed-order agent 代码里,重新跑:
- original fixed order
- risk-aware candidate B
然后把结果汇总成一张表。
因为 candidate A 和 original 顺序相同,所以它本轮不用重复跑,只保留为配置来源说明即可。
7. 读取数据划分与最新模型结果¶
这里不再使用旧 notebook 里手写的分数字典,而是直接读取当前已经保存好的最新结果文件。这样本轮比较会和你现在的实验资产保持一致。
DATA_DIR = PROJECT_ROOT / 'data' / 'HAM10000'
SPLIT_DIR = DATA_DIR / 'splits'
BASELINE_RESULTS_PATH = SUPPORT_DIR / 'baseline_results.csv'
train_df = pd.read_csv(SPLIT_DIR / 'train.csv')
val_df = pd.read_csv(SPLIT_DIR / 'val.csv')
test_df = pd.read_csv(SPLIT_DIR / 'test.csv')
model_registry_df = pd.read_csv(MODEL_REGISTRY_PATH)
baseline_df = pd.read_csv(BASELINE_RESULTS_PATH)
len(train_df), len(val_df), len(test_df)
(7002, 1532, 1481)
image_only_row = baseline_df[baseline_df['Method'] == 'Image Only'].iloc[0]
policy_model_scores = {
'image_only': {'macro_f1': float(image_only_row['Macro-F1'])},
}
report_model_scores = {
'image_only': {
'accuracy': float(image_only_row['Accuracy']),
'balanced_accuracy': float(image_only_row['Balanced Accuracy']),
'macro_f1': float(image_only_row['Macro-F1']),
}
}
for _, row in model_registry_df.iterrows():
key = str(row['Method'])
policy_model_scores[key] = {'macro_f1': float(row['Best Val Macro-F1'])}
report_model_scores[key] = {
'accuracy': float(row['Test Accuracy']),
'balanced_accuracy': float(row['Test Balanced Accuracy']),
'macro_f1': float(row['Test Macro-F1']),
}
policy_model_scores, report_model_scores['image_only']
({'image_only': {'macro_f1': 0.6083234281767498},
'image_age': {'macro_f1': 0.5926280556025649},
'image_sex': {'macro_f1': 0.5959471751891948},
'image_location': {'macro_f1': 0.6041929011696215},
'image_age_sex': {'macro_f1': 0.5964635518225668},
'image_age_location': {'macro_f1': 0.609501496398334},
'image_sex_location': {'macro_f1': 0.6059856792062019},
'image_all_metadata': {'macro_f1': 0.6112034383440658}},
{'accuracy': 0.7771775827143822,
'balanced_accuracy': 0.6372819453123182,
'macro_f1': 0.6083234281767498})
8. 复用 validated fixed-order agent 的状态逻辑¶
下面这部分直接沿用你之前 validated agent 的状态定义,只把固定顺序从写死改成可配置。
def build_initial_state(row):
return {
'image_id': row['image_id'],
'true_label': row['dx'],
'known_metadata': {},
'asked_questions': [],
'done': False,
}
def ask_question(state, row, question):
assert question in ['age', 'sex', 'location']
new_state = {
'image_id': state['image_id'],
'true_label': state['true_label'],
'known_metadata': dict(state['known_metadata']),
'asked_questions': list(state['asked_questions']),
'done': state['done'],
}
if question == 'age':
new_state['known_metadata']['age'] = row['age']
elif question == 'sex':
new_state['known_metadata']['sex'] = row['sex']
elif question == 'location':
new_state['known_metadata']['location'] = row['localization']
new_state['asked_questions'].append(question)
return new_state
def get_model_key_from_known_set(known_set):
if known_set == set():
return 'image_only'
if known_set == {'age'}:
return 'image_age'
if known_set == {'sex'}:
return 'image_sex'
if known_set == {'location'}:
return 'image_location'
if known_set == {'age', 'sex'}:
return 'image_age_sex'
if known_set == {'age', 'location'}:
return 'image_age_location'
if known_set == {'sex', 'location'}:
return 'image_sex_location'
if known_set == {'age', 'sex', 'location'}:
return 'image_all_metadata'
raise ValueError(f'Unknown known_set: {known_set}')
def get_model_key_from_state(state):
return get_model_key_from_known_set(set(state['known_metadata'].keys()))
def score_state(state):
model_key = get_model_key_from_state(state)
return policy_model_scores[model_key]['macro_f1']
9. 可配置的 fixed-order policy¶
这里的关键改动只有一个:question_order 不再写死,而是作为参数传入。
def configurable_fixed_order_policy(state, question_order, max_questions=2):
if len(state['asked_questions']) >= max_questions:
return 'diagnose'
for question in question_order:
if question not in state['asked_questions']:
return question
return 'diagnose'
def run_fixed_order_episode(row, question_order, max_questions=2):
state = build_initial_state(row)
trajectory = []
while not state['done']:
action = configurable_fixed_order_policy(state, question_order=question_order, max_questions=max_questions)
trajectory.append({
'step': len(trajectory),
'action': action,
'model_key_before_action': get_model_key_from_state(state),
'score_before_action': score_state(state),
'known_metadata_before_action': dict(state['known_metadata']),
})
if action == 'diagnose':
state['done'] = True
state['final_action'] = 'diagnose'
state['final_model_key'] = get_model_key_from_state(state)
state['final_score'] = score_state(state)
break
state = ask_question(state, row, action)
return trajectory, state
def evaluate_fixed_order_agent(agent_name, question_order, max_questions=2):
records = []
for idx in range(len(test_df)):
row = test_df.iloc[idx]
trajectory, final_state = run_fixed_order_episode(
row,
question_order=question_order,
max_questions=max_questions,
)
records.append({
'image_id': row['image_id'],
'true_label': row['dx'],
'asked_questions': list(final_state['asked_questions']),
'num_questions': len(final_state['asked_questions']),
'final_model_key': final_state['final_model_key'],
'final_policy_score': final_state['final_score'],
'known_metadata': dict(final_state['known_metadata']),
'question_order': question_order,
'agent_name': agent_name,
})
cases_df = pd.DataFrame(records)
summary_df = cases_df.groupby('final_model_key').size().reset_index(name='count')
summary_df['ratio'] = summary_df['count'] / len(cases_df)
summary_df['test_accuracy'] = summary_df['final_model_key'].map(lambda k: report_model_scores[k]['accuracy'])
summary_df['test_balanced_accuracy'] = summary_df['final_model_key'].map(lambda k: report_model_scores[k]['balanced_accuracy'])
summary_df['test_macro_f1'] = summary_df['final_model_key'].map(lambda k: report_model_scores[k]['macro_f1'])
result = {
'agent_name': agent_name,
'question_order': question_order,
'max_questions': max_questions,
'avg_questions': float(cases_df['num_questions'].mean()),
'expected_accuracy': float((summary_df['ratio'] * summary_df['test_accuracy']).sum()),
'expected_balanced_accuracy': float((summary_df['ratio'] * summary_df['test_balanced_accuracy']).sum()),
'expected_macro_f1': float((summary_df['ratio'] * summary_df['test_macro_f1']).sum()),
}
return cases_df, summary_df, result
10. 正式跑 3 个顺序¶
这里虽然 original 和 candidate_a 顺序相同,但我还是一起跑一遍,方便你后面直接把表拿去用。
all_agent_cases = []
all_agent_summaries = []
all_agent_results = []
for item in comparison_orders:
cases_df, summary_df, result = evaluate_fixed_order_agent(
agent_name=item['agent_name'],
question_order=item['question_order'],
max_questions=2,
)
summary_df = summary_df.copy()
summary_df['agent_name'] = item['agent_name']
summary_df['question_order'] = str(item['question_order'])
all_agent_cases.append(cases_df)
all_agent_summaries.append(summary_df)
all_agent_results.append(result)
results_df = pd.DataFrame(all_agent_results)
results_df
| agent_name | question_order | max_questions | avg_questions | expected_accuracy | expected_balanced_accuracy | expected_macro_f1 | |
|---|---|---|---|---|---|---|---|
| 0 | original_fixed_order | [age, sex, location] | 2 | 2.0 | 0.788656 | 0.585681 | 0.583705 |
| 1 | risk_aware_candidate_a | [age, sex, location] | 2 | 2.0 | 0.788656 | 0.585681 | 0.583705 |
| 2 | risk_aware_candidate_b | [age, location, sex] | 2 | 2.0 | 0.777178 | 0.573000 | 0.580037 |
11. 和旧 validated baselines 放到一起看¶
这一步把新跑的 risk-aware fixed-order 结果,和之前的 fixed_order / uncertainty / lookahead 放到同一张表里。
validated_subset_df = validated_agent_df.copy()
validated_subset_df['question_order'] = validated_subset_df.get('question_order', np.nan)
comparison_df = pd.concat([
validated_subset_df[['agent_name', 'max_questions', 'avg_questions', 'expected_accuracy', 'expected_balanced_accuracy', 'expected_macro_f1', 'threshold']],
results_df.assign(threshold=np.nan)[['agent_name', 'max_questions', 'avg_questions', 'expected_accuracy', 'expected_balanced_accuracy', 'expected_macro_f1', 'threshold']]
], ignore_index=True)
comparison_df
| agent_name | max_questions | avg_questions | expected_accuracy | expected_balanced_accuracy | expected_macro_f1 | threshold | |
|---|---|---|---|---|---|---|---|
| 0 | fixed_order | 2 | 2.0 | 0.793383 | 0.582281 | 0.581666 | NaN |
| 1 | uncertainty | 2 | 2.0 | 0.793383 | 0.582281 | 0.581666 | 0.605 |
| 2 | lookahead | 2 | 2.0 | 0.792708 | 0.598458 | 0.596497 | NaN |
| 3 | original_fixed_order | 2 | 2.0 | 0.788656 | 0.585681 | 0.583705 | NaN |
| 4 | risk_aware_candidate_a | 2 | 2.0 | 0.788656 | 0.585681 | 0.583705 | NaN |
| 5 | risk_aware_candidate_b | 2 | 2.0 | 0.777178 | 0.573000 | 0.580037 | NaN |
12. 保存结果¶
所有结果都按时间戳保存,方便后面回顾和写论文。
run_timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
cases_path = SUPPORT_DIR / f'{run_timestamp}_risk_aware_fixed_order_cases.csv'
summary_path = SUPPORT_DIR / f'{run_timestamp}_risk_aware_fixed_order_summary.csv'
results_path = SUPPORT_DIR / f'{run_timestamp}_risk_aware_fixed_order_results.csv'
comparison_path = SUPPORT_DIR / f'{run_timestamp}_risk_aware_fixed_order_vs_baselines.csv'
pd.concat(all_agent_cases, ignore_index=True).to_csv(cases_path, index=False)
pd.concat(all_agent_summaries, ignore_index=True).to_csv(summary_path, index=False)
results_df.to_csv(results_path, index=False)
comparison_df.to_csv(comparison_path, index=False)
print(cases_path)
print(summary_path)
print(results_path)
print(comparison_path)
/Users/applesues01/Documents/Medical_Agent/supports/2026-08-04_164518_risk_aware_fixed_order_cases.csv /Users/applesues01/Documents/Medical_Agent/supports/2026-08-04_164518_risk_aware_fixed_order_summary.csv /Users/applesues01/Documents/Medical_Agent/supports/2026-08-04_164518_risk_aware_fixed_order_results.csv /Users/applesues01/Documents/Medical_Agent/supports/2026-08-04_164518_risk_aware_fixed_order_vs_baselines.csv