KTern.AI 从传统 SaaS 平台转向智能体 AI 平台时,面对的并不是简单地给产品接入一个大模型。SAP 转型项目往往持续数月甚至更久,涉及需求分析、流程设计、测试、迁移和上线等阶段。系统需要让多个专业智能体协同工作,同时保留持久上下文、限制工具权限,并达到企业生产环境要求的可靠性。
KTern.AI 选择 Amazon Bedrock AgentCore 承载智能体运行能力,并使用 Strands Agents SDK 组织智能体。这个架构的价值不只在于生成回答,而在于让智能体能够跨会话持续执行任务,并在受控边界内调用企业工具。
从单轮问答转向长期运行的智能体系统
普通聊天应用通常把一次请求视为完整事务:接收问题、调用模型、返回答案。企业项目智能体的生命周期则长得多。
例如,一个负责 SAP 测试的智能体可能需要读取业务流程和测试计划,生成测试用例,等待人工审批,再调用测试管理系统创建任务。几天后,它还要根据执行结果定位失败原因。这里至少存在三类状态:
- 对话状态:用户提出了什么要求,智能体给出了什么结论。
- 项目状态:当前处于哪个阶段,哪些工件已经确认,哪些任务仍被阻塞。
- 执行状态:工具调用是否成功,是否需要重试,人工审批是否完成。
因此,持久上下文不能只是不断增长的聊天记录。生产系统通常需要把稳定事实、临时推理和工具执行记录分开保存。稳定事实可以进入项目知识或结构化状态;临时推理应控制生命周期;工具记录则需要可审计、可重放,并带有明确的任务标识。
AgentCore 在这里承担智能体运行平台的角色,而 Strands Agents SDK 用来定义和编排智能体。根据来源摘要可以确认,KTern.AI 正是围绕持久上下文、安全工具访问和生产可靠性构建了这套系统。至于具体的存储结构、服务组合与智能体清单,摘要没有给出完整细节,实施时应根据自身 SAP 系统边界设计。
专业智能体必须有清晰的职责边界
多智能体系统并不是把更多模型放进同一条工作流。每个智能体都应拥有明确的输入、输出和权限。
在类似的 SAP 转型平台中,可以这样划分职责,以下是可供实践的示例,并非对 KTern.AI 内部智能体清单的复述:
| 智能体 | 主要输入 | 主要输出 | 允许调用的工具 |
|---|---|---|---|
| 流程分析智能体 | 流程文档、访谈记录 | 流程差异与待确认问题 | 文档检索、项目知识库 |
| 测试设计智能体 | 业务流程、验收标准 | 测试场景与测试用例 | 测试管理 API |
| 数据迁移智能体 | 字段映射、质量规则 | 校验结果与异常清单 | 只读数据查询、校验任务 |
| 项目协调智能体 | 里程碑、依赖、任务状态 | 风险提示与后续任务 | 项目管理 API、通知服务 |
权限应按智能体身份配置,而不是让所有智能体共享一组万能凭证。测试设计智能体可以创建草稿测试用例,却不应直接修改生产 SAP 数据;迁移智能体可以执行只读校验,却不应在没有审批的情况下触发正式导入。
协调智能体也不应随意转发整段上下文。更稳妥的方式是传递结构化任务,例如:
{
"task_id": "TEST-2025-0412",
"project_id": "S4-MIGRATION-01",
"assigned_agent": "test_designer",
"objective": "为订单到收款流程生成回归测试草稿",
"artifact_refs": ["process://order-to-cash/v7"],
"approval_required": true
}
这样的任务契约便于验证字段、执行权限检查,也能在任务失败时定位责任边界。
一个可运行的持久状态与工具白名单示例
下面的代码不冒充 AgentCore 或 Strands Agents SDK 的真实 API,而是一个可以直接运行的最小示例,用来展示三项关键机制:持久任务状态、按智能体限制工具,以及对写操作设置人工审批。接入实际平台时,可以保留这些边界,再把本地状态和工具实现替换成 AgentCore、Strands 智能体及企业 API。
将代码保存为 agent_workflow.py,使用 Python 3.10 或更高版本运行。它只依赖标准库。
from __future__ import annotations
import json
import sys
import uuid
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Callable
STATE_FILE = Path("agent_state.json")
@dataclass
class Task:
task_id: str
agent: str
objective: str
status: str = "pending"
approval_granted: bool = False
result: dict[str, Any] | None = None
def search_project_docs(query: str) -> dict[str, Any]:
return {
"query": query,
"matches": ["process://order-to-cash/v7"],
}
def create_test_draft(process_ref: str) -> dict[str, Any]:
return {
"draft_id": f"DRAFT-{uuid.uuid4().hex[:8]}",
"process_ref": process_ref,
"cases": [
"Create a sales order",
"Post goods issue",
"Generate an invoice",
],
}
TOOLS: dict[str, Callable[..., dict[str, Any]]] = {
"search_project_docs": search_project_docs,
"create_test_draft": create_test_draft,
}
AGENT_POLICIES = {
"process_analyst": {"search_project_docs"},
"test_designer": {"search_project_docs", "create_test_draft"},
}
WRITE_TOOLS = {"create_test_draft"}
def load_tasks() -> dict[str, Task]:
if not STATE_FILE.exists():
return {}
records = json.loads(STATE_FILE.read_text(encoding="utf-8"))
return {task_id: Task(**data) for task_id, data in records.items()}
def save_tasks(tasks: dict[str, Task]) -> None:
payload = {task_id: asdict(task) for task_id, task in tasks.items()}
STATE_FILE.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def invoke_tool(task: Task, tool_name: str, **arguments: str) -> dict[str, Any]:
allowed_tools = AGENT_POLICIES.get(task.agent, set())
if tool_name not in allowed_tools:
raise PermissionError(f"{task.agent} cannot call {tool_name}")
if tool_name in WRITE_TOOLS and not task.approval_granted:
raise PermissionError(f"{tool_name} requires human approval")
return TOOLS[tool_name](**arguments)
def run(task: Task) -> None:
task.status = "running"
search = invoke_tool(task, "search_project_docs", query=task.objective)
process_ref = search["matches"][0]
if not task.approval_granted:
task.status = "waiting_for_approval"
task.result = {"proposed_process_ref": process_ref}
return
task.result = invoke_tool(
task,
"create_test_draft",
process_ref=process_ref,
)
task.status = "completed"
def main() -> None:
tasks = load_tasks()
if len(sys.argv) == 1:
task = Task(
task_id=str(uuid.uuid4()),
agent="test_designer",
objective="Create regression tests for order to cash",
)
tasks[task.task_id] = task
run(task)
save_tasks(tasks)
print(json.dumps(asdict(task), indent=2))
print(f"Approve with: python {sys.argv[0]} {task.task_id}")
return
task_id = sys.argv[1]
task = tasks[task_id]
task.approval_granted = True
run(task)
save_tasks(tasks)
print(json.dumps(asdict(task), indent=2))
if __name__ == "__main__":
main()
第一次执行会创建任务,并停在等待审批状态:
python agent_workflow.py
从输出中取得 task_id,再批准并恢复任务:
python agent_workflow.py YOUR_TASK_ID
迁移到真实系统时,应重点替换四个部分:
- 将
agent_state.json替换成支持并发控制、加密和恢复机制的持久存储。 - 将本地函数替换成由 AgentCore 运行环境暴露或管理的企业工具。
- 使用 Strands Agents SDK 定义模型、智能体指令、工具和多智能体协作流程。
- 将布尔审批字段替换成带审批人、时间、范围和不可篡改审计记录的授权对象。
生产可靠性来自约束,而不是更长的提示词
企业智能体最危险的故障通常不是回答不够流畅,而是重复执行写操作、读取了不该访问的数据,或在上下文过期后继续使用旧结论。上线前至少应检查以下事项:
- 幂等性:每次工具调用携带任务 ID 和幂等键,重试不能重复创建测试、工单或迁移批次。
- 最小权限:按项目、租户、智能体和工具动作共同授权,避免共享高权限服务账号。
- 上下文隔离:不同客户、项目和环境的记忆必须分区,检索结果也要执行访问控制。
- 人工关卡:数据写入、正式发布、迁移执行和权限变更等高风险动作必须经过审批。
- 可观测性:记录模型请求、工具参数摘要、延迟、重试、审批和最终产物,但要过滤凭证与敏感业务数据。
- 失败恢复:长任务应保存检查点,能够从已确认步骤恢复,而不是从头重新推理和执行。
KTern.AI 的实践说明,从 SaaS 升级为智能体平台的核心工作是建立一个可持续运行的控制系统:专业智能体各司其职,持久上下文维持项目连续性,安全工具将推理连接到实际业务,运行平台负责可靠执行。团队采用类似架构时,适合先选择一个输出可复核、写权限较低的 SAP 场景验证,例如测试草稿或项目风险汇总;等权限模型、审计链路和恢复机制稳定后,再逐步开放更高影响的自动化动作。