向语言模型提问只是最简单的部分。智能体走得更远。模型决定调用你的哪些函数,你的应用程序运行它们,结果回到模型,循环一直持续到模型给出答案。写出这个循环只是开始。一个已发布的应用程序还必须让模型远离不该碰的东西,让个人数据留在本机,在提供商淘汰某个模型的那一天依然正常工作,并且清楚每个用户花了多少钱。
sgcWebSockets 2026.11 为 sgcAI 组件加入了完成这一切所需的部件,适用于 Delphi 7 到 13:
TsgcAIAgent在TsgcAIChat的全部七家提供商上运行工具循环:OpenAI、Anthropic、Gemini、DeepSeek、Ollama、Grok 和 Mistral。它的工具是你的 Delphi 方法和 MCP 服务器的工具,并且在工具修改数据之前会先询问。TsgcAIGuardrails决定哪些工具可以运行,在请求之前对个人数据脱敏并在答案中还原,还会按 JSON schema 检查结构化答案。- MCP 工具固定(tool pinning)会拦截定义在你信任之后发生变化的服务器工具。
- 模型别名和提供商 fallback 让已发布的 exe 在模型被淘汰或提供商宕机时仍能作答。
TsgcAIUsageMeter统计令牌、计价、执行预算,并把每个请求发送到 OpenTelemetry。
一个组件搞定智能体循环
给智能体一个配置好的 TsgcAIChat,描述一个工具,然后调用 Run。智能体会连同工具定义一起发送提示词,运行模型请求的每个工具,把结果送回模型,并重复这一过程,直到模型用纯文本作答。Run 返回的就是这个答案。
oChat := TsgcAIChat.Create(nil);
oChat.Provider := aicpAnthropic;
oChat.ChatOptions.ApiKey := 'API_KEY';
oChat.ChatOptions.Model := 'claude-opus-5-5';
oAgent := TsgcAIAgent.Create(nil);
oAgent.Chat := oChat;
oTool := oAgent.Tools.Add('get_weather', 'Current weather of a city');
oTool.AddParam('city', aatpString, 'City name');
oTool.ReadOnly := True;
oTool.OnExecute := DoGetWeather;
Memo1.Lines.Add(oAgent.Run('Do I need an umbrella in Paris today?'));
工具接收模型写出的 JSON 对象作为参数,并以文本形式返回结果,通常是 JSON:
procedure TForm1.DoGetWeather(Sender: TObject; const aArguments: string;
var aResult: string);
begin
// aArguments is a JSON object, for example {"city":"Paris"}
aResult := '{"city":"Paris","celsius":18,"sky":"rain"}';
end;
智能体构建在 TsgcAIChat.ChatWithTools 之上,后者现在在每家提供商上都支持工具调用。从托管模型切换到本地 Ollama 模型,只需更改 Provider 和 Model,工具保持不变。如果你更愿意自己运行循环,ChatWithTools 和 AddToolResult 也是公开的。
控制循环
Options.MaxSteps 限制一次运行中的模型请求数,Cancel 可以从按钮、定时器或另一个线程停止运行,StopReason 告诉你运行为何结束。当答案被令牌上限截断时,运行会以 aasrMaxTokens 停止,该答案中的工具调用不会执行,因为其参数可能不完整。OnStep 会在每一步发生时报告,因此用户可以看到智能体正在做什么。
oAgent.Options.MaxSteps := 5;
oAgent.OnStep := oAgentStep;
vAnswer := oAgent.Run('Plan a three day trip to Rome');
case oAgent.StopReason of
aasrCompleted:
Memo1.Lines.Add(vAnswer);
aasrMaxSteps:
Memo1.Lines.Add('Stopped after 5 model requests');
aasrMaxTokens:
Memo1.Lines.Add('Cut by the token limit (' + oChat.LastFinishReason + ')');
aasrCancelled:
Memo1.Lines.Add('Cancelled');
end;
procedure TForm1.oAgentStep(Sender: TObject; const aStep: TsgcAIAgentStep);
begin
case aStep.Kind of
aaskToolCall:
Memo1.Lines.Add('Calling ' + aStep.ToolName + ' ' + aStep.Arguments);
aaskToolResult, aaskToolDenied:
Memo1.Lines.Add(aStep.ToolName + ': ' + aStep.Content);
aaskFinal:
Memo1.Lines.Add('Answer: ' + aStep.Content);
end;
end;
工具失败不会结束运行。它的异常会以错误文本的形式回到模型,模型可以尝试其他办法。聊天本身的错误,例如网络故障,会以 aasrError 结束运行,异常会传到你的代码。OnUsage 报告该次运行中每个模型请求的令牌数。
把你的 Delphi 方法用作工具
工具由名称、描述、带类型的参数和处理程序组成,如上所示。当你已经有一个能完成任务的方法时,AddMethod 会原样发布它。工具名称就是方法名,参数、类型和结果通过扩展 RTTI 读取,因此这一项需要 Delphi 2010 或更高版本。
function TForm1.Forecast(const aCity: string; aDays: Integer): string;
begin
Result := Format('%s: sunny for %d days', [aCity, aDays]);
end;
oAgent.Tools.AddMethod(Self, 'Forecast',
'Weather forecast of a city for the next days');
对于输入更复杂的工具,把 InputSchema 设为你自己的 JSON schema。Enabled 可以让工具退出之后的运行,而不必移除它。
来自你的 MCP 服务器的工具
把一个或多个 MCP 客户端添加到 MCPClients,智能体就会在你自己的工具之外提供这些服务器的工具。它们在每次运行中只列出一次,并在模型请求时通过客户端调用。服务器用 readOnlyHint 标记的工具视为只读,当本地工具和服务器工具同名时,本地工具优先。
oMCP := TsgcWSAPIClient_MCP.Create(nil);
oMCP.MCPOptions.HttpOptions.URL := 'http://127.0.0.1:8080/mcp';
oAgent.MCPClients.Add(oMCP);
Memo1.Lines.Add(oAgent.Run('List the support tickets opened today'));
任何更改之前先批准
每个工具都有一个 ReadOnly 标志。只读工具在模型请求时立即运行。修改数据、发送邮件或花钱的工具保持非只读,智能体会在运行之前询问 OnToolApproval,并给出工具名称和模型选择的确切参数。
oTool := oAgent.Tools.Add('send_reminder', 'Sends a payment reminder');
oTool.AddParam('invoice', aatpString, 'Invoice number');
oTool.OnExecute := DoSendReminder; // ReadOnly is False: it changes data
oAgent.OnToolApproval := oAgentToolApproval;
procedure TForm1.oAgentToolApproval(Sender: TObject; const aToolName,
aArguments: string; var aApproved: Boolean);
begin
aApproved := MessageDlg('Run ' + aToolName + ' with ' + aArguments + '?',
mtConfirmation, [mbYes, mbNo], 0) = mrYes;
end;
被拒绝的工具不是错误。模型会被告知该工具未获批准,并可以不使用它来回答用户。像下一节那样关联了 guardrails 后,需要批准却没有处理程序的工具会被拒绝,因此忘记写事件也不会放行写入类工具。
Guardrails
TsgcAIGuardrails 是一个独立的组件,你把它关联到聊天组件,因此同样的规则同时适用于智能体和普通聊天请求。它有三项职责。
- 工具规则。
AllowedTools和DeniedTools接受带*通配符的名称,拒绝始终优先。不被允许的工具永远不会提供给模型。ToolApproval选择哪些工具需要批准:无、非只读的工具,或全部。 - 个人数据。启用
PII.Enabled后,邮箱地址、电话号码、银行卡号、IBAN、IP 地址和你自己的模式会在请求离开应用程序之前被占位符替换,原始内容会在答案中放回去。 - 答案 schema。启用
Schema.Enabled后,与 JSON schema 不符的结构化答案会被拒绝,并给出错误路径,例如$.price: expected number,OnSchemaViolation会通知你。
oGuard := TsgcAIGuardrails.Create(nil);
// tools: reading is allowed, deleting never
oGuard.AllowedTools.Add('get_*');
oGuard.AllowedTools.Add('list_*');
oGuard.AllowedTools.Add('send_reminder');
oGuard.DeniedTools.Add('*delete*');
oGuard.ToolApproval := aigaNotReadOnly;
// personal data never leaves the application
oGuard.PII.Enabled := True;
oGuard.PII.CustomPatterns.Add('EmployeeId=EMP-####');
// structured answers must match the schema
oGuard.Schema.Enabled := True;
oGuard.Schema.JSONSchema := '{"type":"object","properties":' +
'{"price":{"type":"number","minimum":0}},"required":["price"]}';
oChat.Guardrails := oGuard;
你可以自己调用脱敏功能,查看提供商将收到什么,以及你的应用程序拿回什么:
vText := oGuard.Redact('Mail bob@example.com, call +34 600 111 222, ' +
'employee EMP-1234');
// Mail [EMAIL_1], call [PHONE_1], employee [EMPLOYEEID_1]
vText := oGuard.Restore(vText);
// Mail bob@example.com, call +34 600 111 222, employee EMP-1234
固定你的 MCP 服务器的工具
模型会把 MCP 工具的描述和输入 schema 当作指令来读取。如果服务器在你信任它之后修改了它们,无论是因为被入侵还是因为所有者做了改动,都可能把新的指令藏进你已经在使用的工具里。这被称为工具投毒(tool poisoning)。
启用工具固定后,TsgcWSAPIClient_MCP 在第一次列出每个工具时,会为其名称、描述、输入 schema 和注解保存一个 SHA-256 指纹。当之后的列表返回了不同的定义时,该工具会被拦截。它会从列表中移除,智能体永远不会提供它,对它的调用会在发送任何内容之前抛出异常。OnMCPToolChanged 让你可以接受这项变更,或让该工具保持拦截状态以便审查,而该服务器的其他工具继续工作。
oMCP.MCPOptions.ToolPinning.Enabled := True;
oMCP.MCPOptions.ToolPinning.FileName := 'mcp_pins.json';
oMCP.OnMCPToolChanged := oMCPMCPToolChanged;
procedure TForm1.oMCPMCPToolChanged(Sender: TObject; const aToolName,
aOldHash, aNewHash: string; var aAccept: Boolean);
begin
// the definition of a trusted tool changed: keep it blocked
// until somebody reviews it, then call oMCP.PinTool(aToolName)
Memo1.Lines.Add('Tool ' + aToolName + ' changed, blocked for review');
aAccept := False;
end;
被淘汰的模型不会让已发布的 exe 失效
模型名称的变化比已安装的应用程序快得多。编译进 exe 的模型名称,会在提供商淘汰它的那一天失效,每位客户都需要一个新的构建。使用模型别名后,代码请求的是 smart 或 fast,由别名决定模型,如果你愿意,也可以决定提供商。列表可以从文件、JSON 或 URL 加载,因此迁移到新模型只需在你的服务器上做一次修改,而不是发布新版本。
oChat.ModelAliases.Add('fast=gpt-6-luna');
oChat.ModelAliases.Add('smart=anthropic:claude-opus-5-5');
oChat.ProviderApiKeys.Add('anthropic=ANTHROPIC_KEY');
oChat.ChatOptions.Model := 'smart';
// or keep the list on your server and change it without a new build
oChat.LoadModelAliasesFromURL('https://config.example.com/ai/models.json');
Fallback 用来应对故障。当提供商因连接错误、HTTP 5xx、HTTP 429 或不认识的模型而失败时,同一个请求会转到列表中的下一个提供商和模型。对话历史、工具和 JSON 答案都会带过去,OnFallback 会告诉你发生了切换。本地 Ollama 模型是理想的最后一道防线。
oChat.Provider := aicpOpenAI;
oChat.ChatOptions.ApiKey := 'OPENAI_KEY';
oChat.ChatOptions.Model := 'gpt-6-luna';
oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpGemini;
oFallback.Model := 'gemini-3-flash';
oFallback.ApiKey := 'GEMINI_KEY';
oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpOllama;
oFallback.Model := 'llama3.1';
oChat.OnFallback := oChatFallback;
procedure TForm1.oChatFallback(Sender: TObject; const aFromProvider,
aFromModel, aToProvider, aToModel, aError: string);
begin
Memo1.Lines.Add(aFromProvider + ' ' + aFromModel + ' failed (' + aError +
'), trying ' + aToProvider + ' ' + aToModel);
end;
了解每个请求的成本
TsgcAIUsageMeter 记录每个请求的令牌数(包括缓存的输入令牌),按你提供的价格表计价,并按用户和会话保存总量。把它关联到聊天组件,并设置 UserId 和 SessionId。因为用量计位于聊天组件上,智能体运行中的每个模型请求也会被统计。
oMeter := TsgcAIUsageMeter.Create(nil);
// example prices per million tokens, use the ones of your contract
oPrice := oMeter.Prices.Add;
oPrice.Provider := 'openai';
oPrice.Model := 'gpt-6*';
oPrice.InputPerMillion := 1.25;
oPrice.OutputPerMillion := 10;
oPrice.CachedPerMillion := 0.125;
oChat.UserId := 'user-1';
oChat.SessionId := 's-42';
oChat.UsageMeter := oMeter;
oChat.Chat('Hello');
Memo1.Lines.Add(Format('user-1 spent %.6f %s and %d tokens',
[oMeter.TotalCost('user-1'), oMeter.Currency, oMeter.TotalTokens('user-1')]));
价格表用通配符匹配提供商和模型,可以从 JSON 或文件加载,因此价格变化同样不需要新的构建。预算把用量计变成一道限制。预算可以覆盖整个应用程序、每个用户或每个会话,周期为一天、一个月或总计,以金额或令牌数计量。预算用完后,下一个请求会在发送之前被拒绝,因此预算也可以让一次智能体运行在中途停止。
// every user can spend 2 USD a day
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsUser;
oBudget.Period := aubpDay;
oBudget.MaxCost := 2;
// the whole application can spend 500 USD a month
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsGlobal;
oBudget.Period := aubpMonth;
oBudget.MaxCost := 500;
try
Memo1.Lines.Add(oAgent.Run('Remind every customer with an unpaid invoice'));
except
on E: EsgcAIChat do
// Usage budget "each user, day" for user-1 exceeded: ...
Memo1.Lines.Add(E.Message);
end;
如果预算只需把 OnBudgetExceeded 当作提醒触发,请把 BlockWhenExceeded 设为 False。对于报表,记录可以导出到 CSV 文件或任意 TDataSet。启用 OpenTelemetry 后,每个请求都会变成遵循 GenAI 语义约定的 span,并在后台发送到你的 OTLP/HTTP 收集器,不会拖慢聊天。
oMeter.OpenTelemetry.Endpoint := 'http://localhost:4318';
oMeter.OpenTelemetry.ServiceName := 'invoice-assistant';
oMeter.OpenTelemetry.Enabled := True;
oMeter.SaveToCSV('usage.csv');
演示:发票助手
Demos\15.AI\02.Applications\11.AIAgent 中新增的 Delphi 演示程序是一个处理未付发票列表的助手。智能体有三个工具。list_unpaid_invoices 和 get_invoice 是只读的,而 send_reminder 会修改数据,因此会先征求你的批准。智能体在后台线程中运行,所以它工作时窗口仍保持响应。该演示还可以让你试用本文的其余内容:被拒绝的工具和 PII 脱敏、带有实时令牌与成本计数器的每会话预算以及 CSV 导出、模型别名、fallback 提供商,以及工具已被固定的 MCP 服务器。
你的编码智能体可以阅读的文档
编码智能体只能为它了解的库写出好代码。sgcAI、sgcHTML、sgcCrypto 以及其他 sgcWebSockets 包中的每个组件,现在都有一个面向语言模型的专属 Markdown 页面,共 224 页。每个页面说明组件的作用并链接到其产品页面,大多数页面还列出主要属性、方法和事件。TsgcAIAgent 的页面 ai-agent.md 还提供完整的使用示例。llms.txt 会把 Claude Code、Copilot、Cursor 和其他智能体指向这些页面,让智能体在写下第一行代码之前先读到真实的 API。
sgcWebSockets 智能体技能也会学习这些新组件。2026.11.0 的 AI 技能为工具循环、工具批准、guardrails、带 fallback 的模型别名和用量计提供了现成的模式,每个模式都来自可以编译的代码。面向 Delphi、C++Builder 和 .NET 的 AI 智能体技能介绍了如何安装它们。
可用性
TsgcAIAgent、TsgcAIGuardrails、TsgcAIUsageMeter、TsgcAIChat 的模型别名与 fallback,以及 MCP 工具固定,将随 sgcWebSockets 2026.11.0 一起发布,面向 Delphi 7 到 13 和 C++Builder。它们是 sgcAI 组件的一部分,包含在 Enterprise 和 All-Access 版本以及独立的 sgcAI 产品中。帮助文档记录了每个属性、方法和事件,并有关于智能体循环、工具、别名与 fallback、PII 脱敏、schema 校验和工具固定的专题页面。
更多内容请阅读 TsgcAIAgent、TsgcAIGuardrails 和 TsgcAIUsageMeter 的组件页面。
延伸阅读
- 在 Delphi 中构建 AI 智能体:函数调用与 MCP 工具使用,逐步讲解智能体循环
- Delphi MCP 客户端
- Delphi 中的 OpenAI、Anthropic 与 Gemini:你该选哪个 AI API?
有疑问或反馈?联系我们。回复你的会是真正编写这些代码的人。
