为什么选防晒霜需要Agent,而不是一次对话

Consumer Reports(CR)每年用一套严谨的流程评测防晒霜:先均匀涂抹一定剂量,再接受紫外线B照射,测量实际SPF值,甚至对标注“防水”的产品进行80分钟浸泡测试。这套方法论告诉开发者一个重要道理:真实世界的评估从来不是单点判断,而是多步骤、多工具的协作过程。

如果把这个问题扔给一个单次对话的AI(比如纯LLM),它会直接给出“选SPF30以上、广谱、看CR评分”之类的通用建议。但用户的真实需求是:

  • 我是油性皮肤,要去海边游泳,需要防水型。
  • 我皮肤易过敏,不能含氧苯酮。
  • 预算有限,最好在50元以内。

这些条件交织在一起,需要产品信息搜索、成分分析、价格对比、用户评价聚合等多个步骤。单次对话无法动态规划工具调用,也无法在检索失败时重试。这正是Agent系统的用武之地——它能像CR技术员一样,按照一个“测试协议”逐步执行,最后给出一份个性化推荐报告。

Agent架构拆解:规划、工具、记忆、执行

我们将防晒霜推荐系统设计为一个多Agent协作架构,包含三个角色:

  • Coordinator Agent:接收用户需求,分解任务,调度子Agent。
  • Searcher Agent:负责调用外部API(如商品搜索、成分数据库、CR评测数据)。
  • Analyzer Agent:对检索结果进行推理、打分、排序,生成最终推荐。

每个Agent内部遵循通用的“规划-工具-记忆-执行”循环(PTME)。

1. 规划(Planning)

收到用户问题“我油皮,要去海边,预算100以内,求推荐防晒霜”后,Coordinator生成一个任务图:

mermaid
1 2 3 4 5 6
graph TD
    A[收到用户请求] --> B[提取需求:肤质/场景/预算/过敏原]
    B --> C[并行搜索:商品列表 + CR评测 + 成分数据库]
    C --> D[过滤:SPF>=30, 防水, 无过敏原]
    D --> E[评分:CR评分*0.5 + 用户评价*0.3 + 价格*0.2]
    E --> F[排序并输出Top3]

这个计划不是硬编码的,而是由LLM根据工具描述动态生成的。实际实现中可以用ReAct框架或Plan-and-Execute模式。

2. 工具(Tools)

每个Agent持有的工具是文档化的函数。例如Searcher Agent拥有:

  • search_products(query, filters):调用电商API(如亚马逊、京东)返回产品列表。
  • get_cr_rating(product_name):从预抓取的CR评测数据库返回SPF实测值和整体分数。
  • check_ingredients(product_name):查成分表,过滤过敏原(如oxybenzone)。

工具必须返回结构化的JSON,便于后续链式调用。

3. 记忆(Memory)

Coordinator Agent维护一个短时记忆(当前会话的上下文),并将每次执行的中间结果存入。例如,当Searcher发现“Coppertone Water Babies SPF50”没有标注防水,会记录到记忆,然后重新规划:要么降低防水权重,要么寻找替代品。此外,长期记忆可以保存用户的历史偏好(如之前选过什么品牌)。

4. 执行(Execution)

执行阶段最关键的是失败重试。CR的测试可以重复三次取平均值,我们的Agent也需要:

  • 如果搜索API超时,重试3次,每次间隔增长。
  • 如果返回结果少于5条,自动放宽价格过滤器(例如从100元升至120元)并重新搜索。
  • 如果成分检查发现冲突(比如用户要求“无香精”但所有防水型都含香精),Agent应主动向用户确认是否接受折中方案。

核心流程伪代码

以下是用Python + LangChain表达的简化版多Agent协作逻辑(仅关键步骤):

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import Tool
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate

# 定义工具
def search_products(query, max_price=100):
    # 调用电商搜索API,返回JSON列表
    return [{"name":"Coppertone Water Babies SPF50", "price":89, "water_resistant":False}, ...]

def get_cr_rating(name):
    # 从CR数据库查询
    return {"spf_measured":52, "overall_score":100}

def check_ingredients(name):
    # 成分检查,返回是否含过敏原
    return {"has_oxybenzone":False, "has_fragrance":False}

tools = [
    Tool(name="SearchProducts", func=search_products, description="Search sunscreen products by price range and filters"),
    Tool(name="GetCRRating", func=get_cr_rating, description="Get Consumer Reports test results for a product"),
    Tool(name="CheckIngredients", func=check_ingredients, description="Check if product contains allergenic ingredients"),
]

# 协调Agent负责分解任务,这里用ReAct一步到位
agent = create_react_agent(
    llm=ChatOpenAI(model="gpt-4", temperature=0),
    tools=tools,
    prompt=PromptTemplate.from_template(
        "你是一个防晒霜推荐专家。用户:{input}\n"
        "请逐步搜索、筛选和比较,最后推荐3个产品并说明理由。"
        "如果某步失败,尝试放宽条件或询问用户。"
    )
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({"input": "我油皮,去海边游泳,预算100元以内,不能含氧苯酮"})
print(result["output"])

关键实现细节与踩坑记录

1. 工具返回的可靠性

CR的评分数据并非实时API,需要提前抓取后本地存储(比如用SQLite)。我们爬取了2026年CR防晒霜评测排行榜的前50名,包括实际SPF值整体评分。对比显示:标注SPF50的产品实测均值在48-55之间,而SPF30的实测值仅28-32。因此Agent应优先推荐高标注SPF的产品,即使预算有限也要至少SPF30。

2. 用户意图理解模糊

“油皮”意味着需要无油配方(oil-free),“去海边”意味着需要防水(water-resistant)。“不能含氧苯酮”是一种常见过敏原。这些都需要Agent在开始时用信息确认循环澄清。我们写了一个“需求提取”提示模板,让LLM输出结构化参数:

json
1 2 3 4 5 6 7
{
  "skin_type": "oily",
  "activity": "swimming",
  "budget_max": 100,
  "must_exclude": ["oxybenzone"],
  "preferred_features": ["water_resistant", "oil_free"]
}

如果LLM无法从用户输入中提取全部字段,会主动追问。

3. 评分权重设计

CR的测试方法给了我们启示:单一SPF值不能代表全部,防水性、舒适度、成分安全都应纳入。我们采用加权和:CR整体分数 × 0.5 + 用户平均评分 × 0.3 + (100 - 价格) × 0.2。注意价格得分归一化到0-100。测试发现,这种权重下,CR高分产品往往排在前面,但用户评价能纠正极少数“高分但不好用”的异常。

4. 失败重试逻辑的实际代码

python
1 2 3 4 5 6 7 8
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def safe_search_products(query, max_price=100):
    result = search_products(query, max_price)
    if len(result) < 5:
        raise ValueError(f"Only got {len(result)} results, need at least 5")
    return result

当搜索失败时,Agent会收到异常,然后由顶层处理器决定:要么调用更宽松的搜索(将预算上浮20%),要么告知用户该价格区间选择有限。

简化版动手实现:你可以在30分钟内跑起来

  1. 准备CR评测数据(这里提供一个CSV样例,包含产品名、实测SPF、整体分数、是否防水)。
  2. 用一个Jupyter Notebook,导入LangChain库(pip install langchain langchain-openai)。
  3. 将上面伪代码中的工具函数替换为真实API调用(可以用Requests爬取公开商品数据,或使用模拟数据)。
  4. 运行Agent,输入如下示例:
    • “我需要适合沙滩玩的防晒霜,敏感肌,50元以内,不要喷雾要乳液。"
  5. 观察Agent如何先搜索,再过滤,最后给出推荐。

如果LLM API成本敏感,可以用本地模型(如Qwen2.5-7B)配合Ollama,效果在简单推理上已经够用。

总结:从CR评测中学到的Agent设计原则

Consumer Reports的防晒霜测试流程完美映射了Agent系统的工作流:标准化协议 + 多工具协作 + 严格的数据采信 + 失败重试。开发者构建任何消费决策型Agent(选手机、选保险、选投资产品)时,都可以借鉴:

  • 定义清晰的“测试步骤”(规划)
  • 为每个步骤准备专用工具(工具调用)
  • 记录每次测试的中间结果(记忆)
  • 当结果异常时,回到上一步重新执行(失败重试)

下次当你给用户构建推荐系统时,别只依赖LLM的“直觉”。像CR技术员那样,设计一个可控、可解释、可复现的Agent流程——这才是AI从“聊天”变成“干活”的关键。

sunscreen_spf_testing_flowchart