LLM / Agentic Systems

大语言模型 LLM 与 Agent:从下一个 token 到可验证行动闭环

LLM 本质上建模条件 token 分布;Agent 则把语言模型、检索、工具调用、状态更新和验证器组合成可审计的任务系统。

Mechanism Lab

动画:LLM 如何变成带检索、工具和验证的 Agent 闭环

动画从 prompt context 进入 token logits,展示模型先预测文本/动作;随后接入 RAG memory、tool schema、execution trace 和 verifier,把一次生成变成可复查的循环。

Step 1 / 5

Context

任务、系统约束、历史记录和检索材料共同进入 context window。

C=[task,system,history,docs]

Animation Control

Reduced-motion users receive the same step states without continuous motion.

01 / 直觉

核心直觉

LLM 不是直接“理解世界”的程序,而是在上下文 C 下对下一个 token 建模:p(x_t | x_{<t}, C)。能力来自大规模预训练、指令微调、偏好优化和上下文学习。

Prompt、system message、历史记录、检索文档和工具返回值都会进入 context window,改变条件分布,而不是给模型增加外部事实保证。

RAG 把开放世界知识从参数记忆转移到可检查文档;工具调用把文本输出约束为结构化 action;验证器把“生成答案”改成“执行、观察、修正”。

一个可靠 Agent 不是更会聊天的 LLM,而是一个带状态机、权限边界、工具 schema、日志、回滚和证据检查的闭环系统。

02 / 数学

从语言模型目标到 Agent 状态转移

01 / 自回归分解

给定上下文 C,LLM 把序列概率拆成每一步的条件 token 概率。生成就是不断采样或选择下一个 token。

p(x_{1:T}|C)=prod_t p_theta(x_t | x_{<t}, C)

02 / 预训练目标

标准 causal LM 用真实下一个 token 的负对数似然训练;梯度让模型提高正确 token 在 softmax 中的概率。

L(theta)=-sum_t log p_theta(x_t^* | x_{<t}, C)

03 / 指令与偏好对齐

指令微调把任务格式、语气和约束写入监督样本;偏好优化让更被人类偏好的回答相对更可能。

maximize log pi_theta(y_good|x) - log pi_theta(y_bad|x)

04 / RAG 边际化

检索器先给 query 找到候选文档 z,再让生成器在文档条件下回答。理想化写法是对文档证据求和。

p(y|q)=sum_z p_eta(z|q) p_theta(y|q,z)

05 / 工具调用动作

Agent 把某些 token 序列约束为结构化 action,例如工具名和参数;环境执行 action 后返回 observation。

a_t={name,args}, o_t=Tool(a_t)

06 / 状态与验证闭环

真正的 Agent 会把 observation、日志和验证结果写回状态;若验证失败,就重新计划或请求人工确认。

s_{t+1}=update(s_t,a_t,o_t,V(o_t))

03 / 代码

Python 演示:最小 RAG + 工具调用 + 验证闭环

这个例子用一个可替换的 fake_llm 展示 Agent 结构。真实系统中,fake_llm 可以替换为任意 LLM API,但 schema、工具执行和验证边界应该保留。

import math
from collections import Counter

DOCUMENTS = [
    {"id": "did", "text": "Difference-in-differences compares treated and control changes over time."},
    {"id": "psm", "text": "Propensity score matching balances observed covariates before comparing outcomes."},
    {"id": "uat", "text": "Universal approximation says a wide neural network can approximate continuous functions."},
]

def tokenize(text):
    return [word.strip(".,:;!?").lower() for word in text.split()]

def vectorize(text):
    return Counter(tokenize(text))

def cosine(a, b):
    shared = set(a) & set(b)
    dot = sum(a[key] * b[key] for key in shared)
    norm_a = math.sqrt(sum(value * value for value in a.values()))
    norm_b = math.sqrt(sum(value * value for value in b.values()))
    return 0.0 if norm_a == 0 or norm_b == 0 else dot / (norm_a * norm_b)

def retrieve(query, k=2):
    qv = vectorize(query)
    ranked = sorted(
        DOCUMENTS,
        key=lambda doc: cosine(qv, vectorize(doc["text"])),
        reverse=True,
    )
    return ranked[:k]

def validate_action(action):
    allowed = {"search": {"query"}, "draft": {"claim", "evidence_ids"}}
    if action.get("name") not in allowed:
        raise ValueError("unknown tool")
    missing = allowed[action["name"]] - set(action.get("arguments", {}))
    if missing:
        raise ValueError(f"missing arguments: {missing}")
    return action

def run_tool(action):
    args = action["arguments"]
    if action["name"] == "search":
        return retrieve(args["query"])
    if action["name"] == "draft":
        ids = ", ".join(args["evidence_ids"])
        return f"{args['claim']} Evidence: {ids}."
    raise ValueError("unreachable")

def fake_llm(state):
    # Replace this with an LLM call. Keep the action schema and verifier outside the model.
    if not state["docs"]:
        return {"name": "search", "arguments": {"query": state["task"]}}
    evidence_ids = [doc["id"] for doc in state["docs"]]
    return {
        "name": "draft",
        "arguments": {
            "claim": "Use RAG before answering empirical-method questions.",
            "evidence_ids": evidence_ids,
        },
    }

def verify(answer, docs):
    cited = {doc["id"] for doc in docs}
    return all(doc_id in answer for doc_id in cited)

state = {"task": "Explain DID and PSM for a research assistant.", "docs": [], "trace": []}

for step in range(3):
    action = validate_action(fake_llm(state))
    observation = run_tool(action)
    state["trace"].append({"action": action, "observation": observation})
    if action["name"] == "search":
        state["docs"] = observation
    else:
        if verify(observation, state["docs"]):
            state["answer"] = observation
            break
        state["task"] += " Cite retrieved evidence explicitly."

print(state["answer"])
print("trace length:", len(state["trace"]))

04 / 案例

案例:StatsPAI 研究助手如何从聊天变成可审计执行

  • 用户提出:“帮我解释 DID 和 PSM,并给出 Stata/R 实操提醒。” 如果只是一次 LLM 生成,答案可能流畅但无法确认依据、版本和执行路径。
  • Agent 版本会先把任务写入 state,检索课程知识页和方法文档,再生成结构化工具调用,例如 search(query)、open_file(path)、run_code(cmd) 或 render_table(model)。
  • 每次工具调用都会产生 observation:检索到的文档、代码输出、表格路径或错误日志。Agent 把 observation 写回上下文,再决定继续检索、写草稿、运行验证还是请求人工确认。
  • 最终答案必须带有可检查证据:引用了哪些知识页、运行了哪些命令、是否通过 route/build/test、哪些假设仍需人类判断。这就是 Agent 与普通聊天机器人的分界线。

05 / 风险

常见误区

把 LLM 的流畅回答当成事实验证。语言模型给的是高概率文本,不是自动证据。
把所有上下文塞进 prompt 而不做检索、引用和版本控制,会让答案无法审计。
让模型自由拼接工具参数而没有 schema 校验,容易造成路径错误、参数遗漏或越权动作。
忽略工具 observation 的真实性和失败状态,例如命令报错后仍继续写成功结论。
没有权限边界和人工确认节点,导致 Agent 可能发送邮件、删除文件或运行昂贵任务。
用“能完成一次 demo”证明 Agent 可靠,而没有 trace、测试、回滚和失败恢复。

参考资料