让 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)goalfindingsplaniteration 等都放在一份 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 / userNameMD5 签名 。我们把它封装成一个函数,再用 @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_suppliersquery_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_SupplierPUR_POOrder)之后,Agent 的每一次行动都有真实单据数据作为证据,verifier 的判定才真正站得住。
  4. LangGraph 是 Goal 模式的天然实现载体 ------StateGraph 的状态、节点、条件边,恰好把"目标收敛循环"画成了一直跑到 END 的图;verifier 节点 + max_iteration 护栏,是落地时必须守住的工程底线。
  5. 选型建议 :任务清晰可判定、路径开放、需要长时间自主推进时(如供应商分析、测试修复、批量迁移、周报生成),首选 Goal 模式;而那些"一步一问、流程固定"的任务,ReAct 或 Plan-and-Execute 反而更省成本。

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

相关推荐
科研小刘带你玩学术1 天前
AI Agent正在改变人工智能应用模式:从工具助手走向自主智能系统
人工智能·大语言模型·未来趋势·智能体·智能系统
大龄码农有梦想1 天前
AI Agent 目前最大的瓶颈是什么?
人工智能·机器学习·ai agent·智能体·ai工作流·智能体平台
xcLeigh1 天前
KingbaseES 的卢智能运维体架构深度拆解
运维·数据库·人工智能·ai·架构·ffmpeg·智能体
喜欢的名字被抢了1 天前
langgraph教程系列-07-让人介入-人在回路
langgraph
糖果店的幽灵1 天前
大模型测评DeepEval快速入门-安全与通用指标详解
人工智能·安全·langgraph·大模型测评·deepeval
cxr8282 天前
Synapse Mind (三维智能认知图谱) 全景深度工程
人工智能·交互·智能体·认知框架
Bruce_Liuxiaowei2 天前
当AI以可验证的方式攻克数学难题——它如何重写了“科研“的成本与门槛
人工智能·智能体
喜欢的名字被抢了3 天前
langgraph教程系列-06-让流程可暂停 - 持久化与 checkpoint
agent·langgraph
喜欢的名字被抢了3 天前
langgraph教程系列-05-给 agent 装上手脚 - 工具调用
agent·教程·langgraph