传统的对话式和 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 句话
- 从"指令序列"到"目标收敛":其它模式跑的是"你安排的步骤",Goal 模式收敛的是"你定义的终点状态"。
- 完成判定权易主:传统模式是人盯着按"继续",Goal 模式是 Agent 自己核验证据、自己喊停。
- 静态计划 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 从"指令跟随器"升级成"状态收敛器":
- 人只定义终点,路径交给 Agent------把用户从"盯着屏幕按继续"里解放出来,变成"设目标、看结果"。
- 它区别于 ReAct / Plan-and-Execute / Loop 的关键 ,不在于"会不会循环",而在于控制权和完成判定权在谁手里:Goal 模式里 Agent 自己基于证据判定"是否完成",并允许动态重规划。
- 工具让 Goal 模式"可判定"落地 :接上金蝶云星空 OpenAPI(
BD_Supplier、PUR_POOrder)之后,Agent 的每一次行动都有真实单据数据作为证据,verifier的判定才真正站得住。 - LangGraph 是 Goal 模式的天然实现载体 ------
StateGraph的状态、节点、条件边,恰好把"目标收敛循环"画成了一直跑到END的图;verifier节点 +max_iteration护栏,是落地时必须守住的工程底线。 - 选型建议 :任务清晰可判定、路径开放、需要长时间自主推进时(如供应商分析、测试修复、批量迁移、周报生成),首选 Goal 模式;而那些"一步一问、流程固定"的任务,ReAct 或 Plan-and-Execute 反而更省成本。
下一回,当你的 Agent 能"自己找路、自己查 ERP、自己判定、自己喊停",你真正要做的事已经只剩一件:把终点定义得足够清晰。