让 Agent 自己跑到终点线:Goal 模式智能体实战

传统的对话式和 ReAct 式 Agent,靠的是"你说一步、它走一步",控制权始终握在人手里。而 Goal 模式(目标驱动模式) 把控制权彻底反转:人类只定义"终点在哪",Agent 自己负责"规划路径、调用外部工具、核查进度、调整策略",直到当前状态收敛到目标状态为止。本文先讲清 Goal 模式是什么、它和其它主流 Agent 模式(Prompt / ReAct / Plan-and-Execute / Loop)的本质区别,然后落地一个 LangGraph + 金蝶云星空 OpenAPI 的实战案例:Agent 自主调用 BD_Supplier(供应商主数据)与 PUR_POOrder(采购订单)WebAPI,完成一份"供应商竞争分析报告"------让你看懂"目标收敛"状态机接上真实 ERP 数据后怎么跑。 目标不是"执行指令",而是"达成状态"------Goal 模式把 Agent 从 "等喂饭" 的助手,变成 "自己找路、自己查系统、自己判定" 的工兵。

一、什么是 Goal 模式?

1.1 一句话定义

Goal 模式 是指:给 Agent 一个高层次、可判定 的目标,Agent 自主地循环执行 ------ 规划 → 行动 → 观察 → 评估 →(未达成则)再规划 ------ 直到它自己判定"当前状态 == 目标状态"才停止,最后交付结果。

关键在于两个词:

  • 目标(Goal):不是一条动作指令,而是一个"终点状态"的描述。例如"产出一份包含资质、报价、交期、准时率的供应商竞争分析报告"。
  • 可判定(Verifiable) :Agent 必须能基于证据 判断目标是否达成------在 ERP 场景里,证据就是从金蝶云星空查回来的真实单据数据 ,而不是模型"凭感觉"。

1.2 一个最到位的类比:Kubernetes Reconcile Loop

Goal 模式在架构上跟 Kubernetes 的控制循环几乎同构:

你告诉 K8s:"我要 3 个 Pod"(期望状态 desired state ),K8s 自己去想办法凑够 3 个,而不是你指定"先启动第 1 个,再启动第 2 个......"(指令序列 )。它每轮检查当前状态 与期望状态的差距,有差距就执行动作收敛,直到一致。

Goal 模式的 Agent 长得一模一样 ------ 只是 K8s 对它收敛的是"Pod 数量"这种精确可计数的状态 ,而 LLM Agent 收敛的是"报告已完整、三家供应商都对比过"这种靠真实数据来判定的状态。不确定性更高,但架构思想完全一致。

1.3 一句话的精神内核

人定义终点,Agent 自己找路并自己喊停。

用户从"驾驶员"变成"导航员",从"盯着屏幕按回车"变成"设个目标等结果"。

二、Goal 模式与其它模式的区别

要理解 Goal 模式,最好把它放到 Agent 模式光谱里对比着看:

2.1 五种模式速览

① Prompt / 单次问答模式

复制代码
用户提问 → LLM 直接回答(无工具、无循环)

一次性响应,无状态、无工具调用,适合问答、翻译等"单跳"任务。

② ReAct 模式(思考-行动-观察循环)

复制代码
User → LLM(思考) → 工具 → 观察 → LLM(再思考) → 工具 → 观察 → ... → 回答

边想边做,但每条指令都由用户触发的单轮对话 驱动,历史全文随每步重复送入,token 成本接近二次增长,完成与否由用户判断。

③ Plan-and-Execute 模式(先规划后执行)

复制代码
Planner(生成静态计划) → Executor(按计划逐步执行) → Replanner(可能调整) → 回答

先做计划再按计划走;用大模型规划、小模型执行降低成本。但计划偏静态,路径一旦定下不轻易改。

④ Loop 模式(定时循环)

复制代码
每隔 N 分钟/固定节奏跑一轮

适合"定时巡检""周期抓取"这类节奏驱动任务,而非状态驱动。

⑤ Goal 模式(目标驱动、目标收敛)

复制代码
Goal → 规划 → 执行 → 验证 →(未达成)重规划 → 执行 → ... → 达成 → 交付

目标即终点状态 ,Agent 自主迭代,完成与否由 Agent 基于证据判断,由"状态差距"驱动。

2.2 一张对比表看透本质

维度 Prompt/问答 ReAct Plan-and-Execute Loop Goal
驱动源 单次提问 用户逐轮指令 前置规划 固定时间 目标状态差距
谁决定"是否完成" 用户 用户 用户/流程 用户 Agent(基于证据)
是否循环迭代 否 是(单轮内) 是(按计划) 是(定时) 是(状态收敛)
灵活性(路径调整) --- 高 低 中 高(可重规划)
典型适用 问答/翻译 工具调用问答 流程明确的任务 周期巡检 长跑、开放、需自主判断
类比 一问一答 边想边走 画好地图再走 跑步机 K8s Reconcile Loop

2.3 关键差异浓缩成 3 句话

  1. 从"指令序列"到"目标收敛":其它模式跑的是"你安排的步骤",Goal 模式收敛的是"你定义的终点状态"。
  2. 完成判定权易主:传统模式是人盯着按"继续",Goal 模式是 Agent 自己核验证据、自己喊停。
  3. 静态计划 vs 动态重规划 :Plan-and-Execute 路径偏静态,Goal 模式允许在每个循环里根据实际情况推翻重来。

三、为什么用 LangGraph 实现 Goal 模式?

LangGraph 把 Agent 表达成带状态的有向图(StateGraph),恰好和 Goal 模式的"状态机收敛"语义完美契合:

  • 显式状态(State) :goal、findings、plan、iteration 等都放在一份 TypedDict 里,天然就是"当前状态"。
  • 节点(Node):规划、执行、验证、重规划各自是一个纯函数,职责单一。
  • 条件边(Conditional Edge) :verifier 判定 → 未达成就回到 executor → 达成就到 END,这张图本身就是"收敛循环"的可视化。
  • Checkpoint:内置持久化,可以在任务中途打断、恢复,适合真实长任务。

一句话:用 LangGraph 写 Goal 模式,就是把"目标收敛循环"画成一张一直跑到 END 的图。

四、实战:LangGraph + 金蝶云星空 OpenAPI 的"供应商竞争分析"Goal Agent

这一版的关键升级:executor 里的外部工具不再是一段假数据函数 ,而是真正通过金蝶云星空 WebAPI 查询 BD_Supplier(供应商) 与 PUR_POOrder(采购订单)。Agent 自主决定"调哪个接口、查哪个供应商",用 ERP 真实数据作为验证目标的证据。

4.1 安装依赖

bash 复制代码
pip install langgraph langchain-openai requests

4.2 定义状态(这就是"当前状态")

python 复制代码
from typing import Annotated, TypedDict, List
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class GoalState(TypedDict):
    goal: str               # 目标:Agent 要收敛到的"终点状态"
    plan: List[str]         # 执行计划(可被重规划覆盖)
    current_step: int       # 当前计划下标
    findings: Annotated[List[str], add_messages]  # 已收集的证据(来自 ERP 的真实数据)
    messages: Annotated[list, add_messages]       # 模型↔工具 的对话/工具调用历史
    done: bool              # 目标是否达成(verifier 判定)
    report: str             # 最终交付物
    iteration: int          # 已迭代轮次(防死循环的安全网)
    max_iteration: int

4.3 封装金蝶云星空 WebAPI:Agent 的"手"

金蝶云星空的 WebAPI 通过 Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery 查询单据,请求头带上 appId / appSecret / acid / lcId / userName 与 MD5 签名 。我们把它封装成一个函数,再用 @tool 暴露给模型。

python 复制代码
import os, json, hashlib, time, requests
from datetime import datetime, timedelta

# ============ 金蝶云星空连接配置(生产环境务必走密钥管理,不要硬编码) ============
KD_GATEWAY  = os.getenv("KD_GATEWAY", "")          # 例:https://your-gw.example.com/K3Cloud/
KD_APP_ID   = os.getenv("KD_APP_ID", "")           # 第三方应用 AppId
KD_APP_SECRET = os.getenv("KD_APP_SECRET", "")     # 第三方应用密钥
KD_ACID     = os.getenv("KD_ACID", "")             # 账套 ID
KD_LCID     = os.getenv("KD_LCID", "2052")         # 账套语言(2052=简体中文)
KD_USER     = os.getenv("KD_USER", "")             # 用户名
KD_MOCK     = not KD_GATEWAY                      # 未配置网关时退回演示数据,方便本地跑通

# ---------- 金蝶签名:MD5(appId + appSecret + acid + timestamp + lcId + userName) 大写 ----------
def _kd_sign() -> tuple[str, str]:
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    raw = f"{KD_APP_ID}{KD_APP_SECRET}{KD_ACID}{ts}{KD_LCID}{KD_USER}"
    return ts, hashlib.md5(raw.encode("utf-8")).hexdigest().upper()

def _mock_rows(form_id: str) -> list:
    """演示数据:配置 KD_GATEWAY 后自动走真实接口。"""
    if form_id == "BD_Supplier":
        return [
            ["FNumber", "FName", "FContact", "FPhone", "FAddress", "FProvince"],
            ["S001", "润华电子", "张工", "13800000001", "深圳宝安", "广东省"],
            ["S002", "恒鑫精密", "李工", "13800000002", "东莞长安", "广东省"],
            ["S003", "众联科技", "王工", "13800000003", "苏州工业园", "江苏省"],
        ]
    return [
        ["FBillNo", "FDate", "FSupplierId.FNumber", "FMaterialId.FName", "FPrice", "FQty", "FDeliveryDate", "FDocumentStatus"],
        ["PO20260718001", "2026-07-18", "S003", "主板PCB-X1", 30, 5000, "2026-07-26", "C"],
        ["PO20260720001", "2026-07-20", "S001", "主板PCB-X1", 32, 3000, "2026-07-30", "A"],
        ["PO20260725001", "2026-07-25", "S002", "主板PCB-X1", 29, 2000, "2026-08-08", "A"],
    ]

def kd_execute_bill_query(form_id: str, field_keys: list, filter_str: str = "", top: int = 50) -> list:
    """通用查询:返回二维数组,首行为字段名(与金蝶 ExecuteBillQuery 返回一致)。"""
    if KD_MOCK:
        return _mock_rows(form_id)
    ts, sign = _kd_sign()
    headers = {
        "Content-Type": "application/json",
        "kd_appid": KD_APP_ID, "kd_appsecret": KD_APP_SECRET,
        "kd_acid": KD_ACID, "kd_lcid": KD_LCID,
        "kd_username": KD_USER, "kd_sign": sign, "kd_timestamp": ts,
    }
    payload = {
        "FormId": form_id,
        "FieldKeys": ",".join(field_keys),
        "FilterString": filter_str,
        "OrderString": "",
        "TopRowCount": top, "StartRow": 0, "Limit": top,
    }
    url = KD_GATEWAY + "Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc"
    resp = requests.post(url, headers=headers, json=payload, timeout=15)
    resp.raise_for_status()
    return resp.json()

# ---------- 用 @tool 把 ERP 能力暴露给模型(函数签名+docstring 自动生成 JSON Schema) ----------
from langchain_core.tools import tool

@tool
def query_suppliers(keyword: str = "") -> str:
    """从金蝶云星空供应商主数据(BD_Supplier)查询供应商列表。keyword 可选,按名称模糊过滤。"""
    filter_str = f"FName like '%{keyword}%'" if keyword else ""
    rows = kd_execute_bill_query("BD_Supplier",
                                 ["FNumber", "FName", "FContact", "FPhone", "FAddress", "FProvince"],
                                 filter_str)
    return json.dumps(rows, ensure_ascii=False)

@tool
def query_supplier_orders(supplier_number: str, days: int = 90) -> str:
    """查询某供应商近 days 天的采购订单(PUR_POOrder):单据号、日期、物料、单价、数量、交货日期、单据状态。"""
    since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
    filter_str = f"FSupplierId.FNumber='{supplier_number}' and FDate>='{since}'"
    rows = kd_execute_bill_query("PUR_POOrder",
                                 ["FBillNo", "FDate", "FSupplierId.FNumber", "FMaterialId.FName",
                                  "FPrice", "FQty", "FDeliveryDate", "FDocumentStatus"],
                                 filter_str)
    return json.dumps(rows, ensure_ascii=False)

tools = [query_suppliers, query_supplier_orders]
tool_map = {t.name: t for t in tools}

# ---------- bind_tools:把 ERP 接口的 Schema 绑给模型,由模型自主决定调哪个 ----------
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage

model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
).bind_tools(tools)

4.4 节点:规划 / 执行 / 验证 / 重规划

python 复制代码
def extract_goal(state: GoalState) -> dict:
    """从用户自然语言中提取可判定的目标与边界。"""
    return {"plan": [], "findings": [], "messages": [],
            "current_step": 0, "done": False,
            "iteration": 0, "max_iteration": 6}

def planner(state: GoalState) -> dict:
    """把目标拆成步骤(生产环境由 LLM 生成,这里用模板保证示例能跑通)。"""
    return {"plan": [
        "查询供应商主数据,列出候选供应商",
        "逐一查询每家供应商近 90 天采购订单,计算报价与准时交付情况",
        "对比各家资质、报价、交期、准时率",
        "生成带综合评级的竞争分析报告",
    ], "current_step": 0}

def executor(state: GoalState) -> dict:
    """Goal 模式的引擎:「模型↔ERP工具」循环------
    模型产生 tool_calls 就执行对应金蝶接口并把结果回喂,直到给出步骤结论。"""
    step = state["plan"][min(state["current_step"], len(state["plan"]) - 1)]
    messages = list(state.get("messages", []))
    produced = []
    new_user = HumanMessage(content=f"当前执行步骤:{step},需要数据时请调用金蝶云星空工具。")
    messages.append(new_user); produced.append(new_user)

    for _ in range(6):                               # 单步内最多 6 次,防止模型反复乱调工具
        ai = model.invoke(messages)
        messages.append(ai); produced.append(ai)
        if not ai.tool_calls:                        # 模型不再想调工具 → 步骤结论就绪
            break
        for tc in ai.tool_calls:                     # 逐条调用真实 ERP 接口
            result = tool_map[tc["name"]].invoke(tc["args"])
            tm = ToolMessage(content=result, tool_call_id=tc["id"])
            messages.append(tm); produced.append(tm)

    evidence = f"[步骤{state['current_step']}] {step} -> {messages[-1].content}"
    return {"findings": [evidence],
            "current_step": state["current_step"] + 1,
            "messages": produced}

def verifier(state: GoalState) -> dict:
    """评估证据是否足以达成目标------这是 Goal 模式的『心脏』。
    生产环境应在此再调用 ERP 工具核验关键数字(如单据状态、订单笔数),而非只看模型表述。"""
    enough = len(state["findings"]) >= len(state["plan"])
    return {"done": enough or state["iteration"] >= state["max_iteration"]}

def replanner(state: GoalState) -> dict:
    """侦查反馈:若某步失败或证据不足,重规划或补充步骤。"""
    if state["current_step"] >= len(state["plan"]):
        return {"plan": state["plan"] + ["汇总评级并输出最终报告"]}
    return {"plan": state["plan"]}

def reporter(state: GoalState) -> dict:
    """生成最终交付物(生产环境由 LLM 基于 findings 归纳)。"""
    return {"report": (
        "【供应商竞争分析】(数据来源:金蝶云星空 BD_Supplier / PUR_POOrder)\n"
        "1. 候选供应商:润华电子(S001)、恒鑫精密(S002)、众联科技(S003)\n"
        "2. 报价对比:恒鑫 29 元 < 众联 30 元 < 润华 32 元\n"
        "3. 准时率/交期:众联 6 天最快且订单状态均为已审核,综合评级最高\n"
        "4. 建议:优先合作众联科技,次选润华电子")}

4.5 条件边:收敛循环

python 复制代码
def should_converge(state: GoalState) -> str:
    """边路由:目标达成 → END;未达成且有剩余迭代 → 继续执行/重规划。"""
    if state["done"] or state["iteration"] + 1 >= state["max_iteration"]:
        return "finish"    # 撞到安全网强制收尾,避免无限循环
    return "keep_going"

builder = StateGraph(GoalState)
builder.add_node("set_goal", extract_goal)
builder.add_node("planner", planner)
builder.add_node("executor", executor)
builder.add_node("verifier", verifier)
builder.add_node("replanner", replanner)
builder.add_node("reporter", reporter)

builder.add_edge(START, "set_goal")
builder.add_edge("set_goal", "planner")
builder.add_edge("planner", "executor")
builder.add_edge("executor", "verifier")
builder.add_conditional_edges("verifier", should_converge,
                              {"finish": "reporter", "keep_going": "replanner"})
builder.add_edge("replanner", "executor")
builder.add_edge("reporter", END)

from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(checkpointer=MemorySaver())   # 支持中断/恢复

4.6 跑起来

python 复制代码
config = {"configurable": {"thread_id": "goal-kd-001"}}
inputs = {"goal": "基于金蝶云星空数据,对比润华、恒鑫、众联三家供应商,产出一份含资质、报价、交期、准时率的竞争分析报告"}

final = graph.invoke(inputs, config)
print(final["report"])

输出大致为:

复制代码
【供应商竞争分析】(数据来源:金蝶云星空 BD_Supplier / PUR_POOrder)
1. 候选供应商:润华电子(S001)、恒鑫精密(S002)、众联科技(S003)
2. 报价对比:恒鑫 29 元 < 众联 30 元 < 润华 32 元
3. 准时率/交期:众联 6 天最快且订单状态均为已审核,综合评级最高
4. 建议:优先合作众联科技,次选润华电子

4.7 读懂这张图中的"Goal 语义"

  • ERP 工具是 Goal 模式的"手"和"眼睛" :@tool 定义的两个金蝶接口(query_suppliers、query_supplier_orders)经 bind_tools 绑给模型后,模型会自己决定查哪个表、按什么条件过滤 。verifier 判定"目标是否达成"所用的证据,正是这些接口返回的真实单据数据------没有 ERP 数据,Goal 模式的"可判定性"就无从谈起。
  • 节点 verifier 就是 Goal 模式的核心 :它把"当前证据 vs 目标"做对比,并自动决定继续还是停下------完成判定权交给了 Agent。
  • replanner 实现动态重规划:某家供应商查不到/数据不足,就追加步骤,而不是死守第一版计划。
  • max_iteration 是安全网,对应"目标有歧义而原地打转"的最坏情况------轮次上限是 Goal 模式必备护栏。

生产化清单:

  • 网关、appSecret、账套等信息走密钥管理(如环境变量/密钥服务),绝不硬编码;
  • 金蝶接口只授最小权限(只读查询),并在网关上限制 IP 白名单与调用频率;
  • 每次外部调用做幂等与超时/重试;对 PUR_POOrder 等单据查询注意字段权限与单据状态过滤(如仅统计已审核 C 状态);
  • planner / verifier / reporter 换成 LLM 调用,并让 verifier 用 ERP 工具二次核验关键数字;
  • 用 Checkpointer 支持长任务中断恢复。

五、总结

Goal 模式的本质,是把 Agent 从"指令跟随器"升级成"状态收敛器":

  1. 人只定义终点,路径交给 Agent------把用户从"盯着屏幕按继续"里解放出来,变成"设目标、看结果"。
  2. 它区别于 ReAct / Plan-and-Execute / Loop 的关键 ,不在于"会不会循环",而在于控制权和完成判定权在谁手里:Goal 模式里 Agent 自己基于证据判定"是否完成",并允许动态重规划。
  3. 工具让 Goal 模式"可判定"落地 :接上金蝶云星空 OpenAPI(BD_Supplier、PUR_POOrder)之后,Agent 的每一次行动都有真实单据数据作为证据,verifier 的判定才真正站得住。
  4. LangGraph 是 Goal 模式的天然实现载体 ------StateGraph 的状态、节点、条件边,恰好把"目标收敛循环"画成了一直跑到 END 的图;verifier 节点 + max_iteration 护栏,是落地时必须守住的工程底线。
  5. 选型建议 :任务清晰可判定、路径开放、需要长时间自主推进时(如供应商分析、测试修复、批量迁移、周报生成),首选 Goal 模式;而那些"一步一问、流程固定"的任务,ReAct 或 Plan-and-Execute 反而更省成本。

下一回,当你的 Agent 能"自己找路、自己查 ERP、自己判定、自己喊停",你真正要做的事已经只剩一件:把终点定义得足够清晰。

相关推荐
小小-猿39 分钟前
从 gpt-6-luna 到第一个 Nexent 智能体:配置、调试与发布
gpt·大模型·智能体·ai智能体·nexent
科研小刘带你玩学术2 小时前
【科研快讯】OpenAI推出ChatGPT Wallet:AI Agent不再只会推荐商品,还能替用户完成支付
人工智能·数字经济·智能体·科技前沿·aiagent·ai支付·智能商务
指尖时光.14 小时前
【Agent】个人 Agent 工作台建设指南
agent·知识库·智能体·skill·agent开发
贾伟康4 天前
HarmonyOS API 26 HMAF Skill 可解释任务计划与失败回退
harmonyos·arkts·智能体·skill·hmaf
贾伟康4 天前
HarmonyOS API 26 HMAF 2.0 意图解析的可解释动作图
harmonyos·arkts·智能体·a2a·hmaf
承渊政道5 天前
LangChainAI应用开发框架【LangChain与LangGraph介绍】
langchain·大模型·langgraph·ai应用开发
贾伟康5 天前
HarmonyOS API 26 HMAF Skill 多 Skill 冲突仲裁与可恢复执行
harmonyos·arkts·智能体·skill·hmaf
OxYGC6 天前
智能体项目复盘 · 智能客服篇:对话栈、Flow 与策略集成器——LLM 是怎么嵌进传统多轮对话架构的
对话系统·智能客服·rag·智能体
正在走向自律7 天前
AI Agent 落地这一年:从“能跑起来”到“敢放出去”
大数据·人工智能·ai agent·智能体·mcp·a2a
积流成海7 天前
Agent演化史(九):多智能体的暗面,当一次 prompt 注入可以横向移动
prompt·agent·智能体·大模型应用