自定义函数开发指南
自定义函数(Custom Function)是 coomia-dip 平台扩展计算能力的核心机制。通过 Python 编写函数并注册到平台,你可以在 Action、Pipeline、Rule 和 Dashboard 中复用业务逻辑。本文覆盖函数的定义、注册、测试、版本管理和生产部署全流程。
Coomia发布于 2026年1月17日13 分钟阅读
分享本文Twitter / X
“系列:S12 开发者教程 · 第 8 篇 | 难度:中级 | 阅读时间:15 分钟
自定义函数开发指南
#TL;DR
自定义函数(Custom Function)是 coomia-dip 平台扩展计算能力的核心机制。通过 Python 编写函数并注册到平台,你可以在 Action、Pipeline、Rule 和 Dashboard 中复用业务逻辑。本文覆盖函数的定义、注册、测试、版本管理和生产部署全流程。
#1. 函数概述
#1.1 什么是自定义函数?
自定义函数是运行在 Intelligence Layer(Reasoning & Decision Layer)上的 Python 代码单元,可以被 Ontology 生态中的其他组件调用。函数支持同步和异步两种模式,通过 gRPC 暴露为平台服务。
Code
┌─────────────┐ gRPC ┌──────────────────┐
│ Action │ ────────── │ │
│ Pipeline │ ────────── │ Function Runtime │
│ Rule Engine │ ────────── │ (Reasoning & Decision Layer) │
│ Dashboard │ ────────── │ │
└─────────────┘ └──────────────────┘
#1.2 函数类型
| 类型 | 用途 | 示例 |
|---|---|---|
| 计算函数 | 数据转换和计算 | 风险评分计算、价格策略 |
| 查询函数 | 复杂数据查询 | 跨对象聚合、图谱遍历 |
| 集成函数 | 外部系统集成 | API 调用、消息推送 |
| 验证函数 | 数据校验 | 业务规则验证、格式检查 |
#2. 环境准备
#2.1 项目结构
Code
my_functions/
├── pyproject.toml
├── py.typed
├── __init__.py
├── functions/
│ ├── __init__.py
│ ├── risk_scoring.py
│ ├── notification.py
│ └── data_validation.py
├── models/
│ ├── __init__.py
│ └── schemas.py
└── tests/
├── __init__.py
├── test_risk_scoring.py
└── test_notification.py
#2.2 依赖配置
TOML
# pyproject.toml
[project]
name = "my-onto-functions"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"ontology-sdk>=1.0.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-asyncio>=0.21",
"black",
"ruff",
"mypy",
]
#3. 编写第一个函数
#3.1 基础函数定义
Python
# functions/risk_scoring.py
from ontology_sdk.functions import function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class RiskLevel(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class RiskInput(BaseModel):
"""风险评分输入参数"""
customer_rid: str = Field(description="客户对象 RID")
assessment_type: str = Field(default="standard", description="评估类型")
class RiskOutput(BaseModel):
"""风险评分输出结果"""
risk_level: RiskLevel
risk_score: float = Field(ge=0, le=100)
risk_factors: list[str]
recommendations: list[str]
confidence: float = Field(ge=0, le=1)
@function(
name="calculate_risk_score",
display_name="客户风险评分",
description="基于客户交易历史和行为数据计算风险评分",
version="1.0.0",
input_type=RiskInput,
output_type=RiskOutput,
tags=["risk", "scoring", "customer"],
)
def calculate_risk_score(ctx: FunctionContext, input: RiskInput) -> RiskOutput:
"""计算客户风险评分"""
# 获取客户数据
customer = ctx.platform.objects.get_by_rid(input.customer_rid)
if not customer:
raise ValueError(f"Customer not found: {input.customer_rid}")
# 获取交易历史
transactions = ctx.platform.oql.execute(f"""
FIND Transaction
TRAVERSE belongs_to -> Customer
WHERE Customer.rid = '{input.customer_rid}'
SELECT amount, transaction_date, type, status
ORDER BY transaction_date DESC
LIMIT 100
""")
# 计算风险因子
risk_factors = []
score = 50.0 # 基准分
# 因子 1:交易频率异常
recent_count = sum(1 for t in transactions
if (ctx.now() - t.transaction_date).days < 7)
if recent_count > 20:
score += 15
risk_factors.append("近7天交易频率异常偏高")
# 因子 2:大额交易占比
large_txns = [t for t in transactions if t.amount > 100000]
if len(large_txns) / max(len(transactions), 1) > 0.3:
score += 20
risk_factors.append("大额交易占比超过30%")
# 因子 3:失败交易率
failed = [t for t in transactions if t.status == "failed"]
fail_rate = len(failed) / max(len(transactions), 1)
if fail_rate > 0.1:
score += 10
risk_factors.append(f"交易失败率 {fail_rate:.1%}")
# 因子 4:客户等级
if customer.tier == "bronze":
score += 5
risk_factors.append("低等级客户")
# 确定风险等级
score = min(100, max(0, score))
if score >= 80:
level = RiskLevel.CRITICAL
elif score >= 60:
level = RiskLevel.HIGH
elif score >= 40:
level = RiskLevel.MEDIUM
else:
level = RiskLevel.LOW
# 生成建议
recommendations = []
if level in (RiskLevel.CRITICAL, RiskLevel.HIGH):
recommendations.append("建议人工审核该客户近期交易")
recommendations.append("启用增强身份验证")
if "大额交易" in str(risk_factors):
recommendations.append("对大额交易启用双重审批流程")
return RiskOutput(
risk_level=level,
risk_score=score,
risk_factors=risk_factors,
recommendations=recommendations,
confidence=0.85,
)
#3.2 异步函数
Python
# functions/notification.py
from ontology_sdk.functions import async_function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional
import httpx
class NotificationInput(BaseModel):
channel: str = Field(description="通知渠道: email/dingtalk/webhook")
recipients: list[str] = Field(description="接收人列表")
subject: str
body: str
template: Optional[str] = None
priority: str = Field(default="normal")
class NotificationOutput(BaseModel):
sent_count: int
failed_count: int
failures: list[dict] = Field(default_factory=list)
@async_function(
name="send_notification",
display_name="发送通知",
description="通过指定渠道发送通知消息",
version="1.0.0",
input_type=NotificationInput,
output_type=NotificationOutput,
timeout_seconds=30,
retry_on_failure=True,
max_retries=3,
)
async def send_notification(
ctx: FunctionContext, input: NotificationInput
) -> NotificationOutput:
"""发送通知到指定渠道"""
sent = 0
failed = 0
failures = []
# 渲染模板
body = input.body
if input.template:
template = await ctx.platform.templates.get(input.template)
body = template.render(body=input.body, subject=input.subject)
async with httpx.AsyncClient() as client:
for recipient in input.recipients:
try:
if input.channel == "email":
await _send_email(client, recipient, input.subject, body)
elif input.channel == "dingtalk":
await _send_dingtalk(client, recipient, input.subject, body)
elif input.channel == "webhook":
await _send_webhook(client, recipient, input.subject, body)
sent += 1
except Exception as e:
failed += 1
failures.append({
"recipient": recipient,
"error": str(e)
})
ctx.logger.warning(f"Failed to send to {recipient}: {e}")
return NotificationOutput(
sent_count=sent,
failed_count=failed,
failures=failures,
)
async def _send_email(client, recipient, subject, body):
await client.post(
"http://mail-service:8080/api/send",
json={"to": recipient, "subject": subject, "body": body}
)
async def _send_dingtalk(client, recipient, subject, body):
await client.post(
f"https://oapi.dingtalk.com/robot/send?access_token={recipient}",
json={"msgtype": "markdown", "markdown": {"title": subject, "text": body}}
)
async def _send_webhook(client, recipient, subject, body):
await client.post(recipient, json={"subject": subject, "body": body})
#4. 函数注册与管理
#4.1 注册函数
Python
from ontology_sdk import OntoPlatform
platform = OntoPlatform(
control_plane_url="localhost:50051",
intelligence_plane_url="localhost:50053"
)
# 从模块注册
platform.functions.register_module("functions.risk_scoring")
platform.functions.register_module("functions.notification")
# 从目录批量注册
platform.functions.register_directory("functions/")
# 查看已注册函数
for fn in platform.functions.list():
print(f"{fn.name} v{fn.version} - {fn.display_name}")
print(f" 输入: {fn.input_schema}")
print(f" 输出: {fn.output_schema}")
#4.2 调用函数
Python
# 同步调用
result = platform.functions.invoke(
"calculate_risk_score",
input={
"customer_rid": "ri.ontology.object.customer.c-001",
"assessment_type": "enhanced"
}
)
print(f"风险等级: {result.risk_level}")
print(f"风险分数: {result.risk_score}")
# 异步调用
import asyncio
async def main():
result = await platform.functions.invoke_async(
"send_notification",
input={
"channel": "dingtalk",
"recipients": ["webhook-token-123"],
"subject": "风险预警",
"body": "客户 XX 风险评分异常升高"
}
)
print(f"发送: {result.sent_count}, 失败: {result.failed_count}")
asyncio.run(main())
# 批量调用
batch_results = platform.functions.invoke_batch(
"calculate_risk_score",
inputs=[
{"customer_rid": f"ri.ontology.object.customer.c-{i:03d}"}
for i in range(1, 101)
],
concurrency=10
)
#5. 函数测试
#5.1 单元测试
Python
# tests/test_risk_scoring.py
import pytest
from unittest.mock import MagicMock, AsyncMock
from functions.risk_scoring import calculate_risk_score, RiskInput, RiskLevel
from ontology_sdk.functions import FunctionContext
@pytest.fixture
def mock_context():
ctx = MagicMock(spec=FunctionContext)
# 模拟客户数据
customer = MagicMock()
customer.tier = "gold"
customer.annual_revenue = 5000000
ctx.platform.objects.get_by_rid.return_value = customer
# 模拟交易数据
ctx.platform.oql.execute.return_value = [
MagicMock(amount=50000, transaction_date=ctx.now() - timedelta(days=1),
type="purchase", status="success"),
MagicMock(amount=200000, transaction_date=ctx.now() - timedelta(days=3),
type="purchase", status="success"),
]
return ctx
class TestRiskScoring:
def test_low_risk_customer(self, mock_context):
"""低风险客户应返回 LOW 等级"""
input_data = RiskInput(
customer_rid="ri.ontology.object.customer.c-001"
)
result = calculate_risk_score(mock_context, input_data)
assert result.risk_level == RiskLevel.LOW
assert result.risk_score < 40
assert result.confidence > 0
def test_high_risk_customer(self, mock_context):
"""大量大额交易的客户应返回高风险"""
# 模拟大量大额交易
from datetime import timedelta
now = mock_context.now()
mock_context.platform.oql.execute.return_value = [
MagicMock(amount=500000, transaction_date=now - timedelta(days=i),
type="purchase", status="success")
for i in range(25)
] + [
MagicMock(amount=100, transaction_date=now - timedelta(days=i),
type="purchase", status="failed")
for i in range(5)
]
input_data = RiskInput(
customer_rid="ri.ontology.object.customer.c-002"
)
result = calculate_risk_score(mock_context, input_data)
assert result.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)
assert result.risk_score >= 60
assert len(result.risk_factors) > 0
assert len(result.recommendations) > 0
def test_customer_not_found(self, mock_context):
"""客户不存在应抛出异常"""
mock_context.platform.objects.get_by_rid.return_value = None
input_data = RiskInput(
customer_rid="ri.ontology.object.customer.nonexistent"
)
with pytest.raises(ValueError, match="Customer not found"):
calculate_risk_score(mock_context, input_data)
#5.2 集成测试
Python
# tests/test_integration.py
import pytest
from ontology_sdk import OntoPlatform
from ontology_sdk.testing import TestPlatform
@pytest.fixture
def test_platform():
"""使用测试平台(内存模式)"""
return TestPlatform(
seed_data={
"Customer": [
{"rid": "c-001", "name": "测试客户A", "tier": "gold"},
{"rid": "c-002", "name": "测试客户B", "tier": "bronze"},
],
"Transaction": [
{"customer_rid": "c-001", "amount": 50000, "status": "success"},
{"customer_rid": "c-001", "amount": 200000, "status": "success"},
{"customer_rid": "c-002", "amount": 500000, "status": "failed"},
]
}
)
class TestRiskScoringIntegration:
def test_end_to_end(self, test_platform):
"""端到端集成测试"""
result = test_platform.functions.invoke(
"calculate_risk_score",
input={"customer_rid": "c-001"}
)
assert result.risk_level is not None
assert 0 <= result.risk_score <= 100
#6. 函数中访问 Ontology
#6.1 读取对象
Python
@function(name="enrich_project_data", ...)
def enrich_project_data(ctx: FunctionContext, input: dict) -> dict:
# 通过 RID 获取对象
project = ctx.platform.objects.get_by_rid(input["project_rid"])
# 通过过滤条件查找
dept = ctx.platform.objects.get(
"Department",
filters={"code": project.department_code}
)
# OQL 查询
team_members = ctx.platform.oql.execute(f"""
FIND Employee
TRAVERSE participates_in -> Project
WHERE Project.rid = '{project.rid}'
SELECT Employee.name, Employee.level, participates_in.role
""")
return {
"project_name": project.name,
"department": dept.name if dept else "Unknown",
"team_size": len(team_members),
"team": [{"name": m.name, "role": m.role} for m in team_members],
}
#6.2 写入对象
Python
@function(name="create_project_report", ...)
def create_project_report(ctx: FunctionContext, input: dict) -> dict:
# 创建新对象
report = ctx.platform.objects.create(
object_type="Report",
properties={
"title": f"项目月报 - {input['month']}",
"project_rid": input["project_rid"],
"generated_at": ctx.now().isoformat(),
"content": generate_report_content(ctx, input),
}
)
# 创建关系
ctx.platform.relations.create(
relation_type="has_report",
source_rid=input["project_rid"],
target_rid=report.rid,
)
return {"report_rid": report.rid, "status": "created"}
#7. 函数版本与热更新
#7.1 版本管理
Python
# 注册新版本
@function(name="calculate_risk_score", version="2.0.0", ...)
def calculate_risk_score_v2(ctx, input):
# 新版本逻辑
pass
# 查看版本列表
versions = platform.functions.list_versions("calculate_risk_score")
for v in versions:
print(f"v{v.version}: {v.status} (部署于 {v.deployed_at})")
# 灰度发布
platform.functions.set_traffic_split("calculate_risk_score", {
"1.0.0": 80, # 80% 流量走旧版本
"2.0.0": 20, # 20% 流量走新版本
})
# 全量切换
platform.functions.promote("calculate_risk_score", version="2.0.0")
# 回滚
platform.functions.rollback("calculate_risk_score", target_version="1.0.0")
#8. 函数在其他组件中的使用
#8.1 在 Action 中调用
YAML
# actions/assess_customer_risk.yaml
name: assess_customer_risk
type: function_call
function: calculate_risk_score
input_mapping:
customer_rid: $trigger.object_rid
output_handling:
- condition: output.risk_level == 'critical'
action: send_notification
params:
channel: dingtalk
subject: "高危客户预警"
body: "客户 ${trigger.object.name} 风险评分: ${output.risk_score}"
#8.2 在 Pipeline Transform 中调用
YAML
transform:
stages:
- name: risk_enrichment
type: function_call
function: calculate_risk_score
input_mapping:
customer_rid: $record.customer_rid
output_mapping:
risk_level: $output.risk_level
risk_score: $output.risk_score
#8.3 在 Dashboard 中调用
Python
# Dashboard widget 数据源
@function(name="dashboard_project_health", ...)
def dashboard_project_health(ctx: FunctionContext, input: dict) -> dict:
projects = ctx.platform.oql.execute("""
FIND Project WHERE status = 'in_progress'
SELECT name, budget, end_date, priority
""")
return {
"total_projects": len(projects),
"by_priority": {
"critical": len([p for p in projects if p.priority == "critical"]),
"high": len([p for p in projects if p.priority == "high"]),
"medium": len([p for p in projects if p.priority == "medium"]),
"low": len([p for p in projects if p.priority == "low"]),
},
"total_budget": sum(p.budget for p in projects),
}
#9. 性能优化
#9.1 缓存策略
Python
from ontology_sdk.functions import function, FunctionContext
from functools import lru_cache
@function(name="get_department_info", cache_ttl=300, ...)
def get_department_info(ctx: FunctionContext, input: dict) -> dict:
"""带缓存的部门信息查询(TTL 5分钟)"""
dept = ctx.platform.objects.get("Department", filters={"code": input["dept_code"]})
return {"name": dept.name, "location": dept.location, "head_count": dept.head_count}
#9.2 并发控制
Python
@function(
name="batch_risk_assessment",
concurrency_limit=20,
timeout_seconds=60,
...
)
def batch_risk_assessment(ctx: FunctionContext, input: dict) -> dict:
"""批量风险评估,限制并发为 20"""
results = []
for customer_rid in input["customer_rids"]:
result = ctx.invoke("calculate_risk_score", {"customer_rid": customer_rid})
results.append(result)
return {"assessments": results}
#10. 完整实战:构建审批函数
Python
from ontology_sdk.functions import function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class ApprovalInput(BaseModel):
request_rid: str
approver_rid: str
decision: str # approve / reject
comment: Optional[str] = None
class ApprovalOutput(BaseModel):
status: str
next_approver: Optional[str] = None
is_final: bool
audit_trail_rid: str
@function(
name="process_approval",
display_name="处理审批",
description="处理审批请求,支持多级审批流程",
version="1.0.0",
input_type=ApprovalInput,
output_type=ApprovalOutput,
)
def process_approval(ctx: FunctionContext, input: ApprovalInput) -> ApprovalOutput:
# 获取审批请求
request = ctx.platform.objects.get_by_rid(input.request_rid)
if not request:
raise ValueError(f"Approval request not found: {input.request_rid}")
# 验证审批人权限
approver = ctx.platform.objects.get_by_rid(input.approver_rid)
if request.current_approver_rid != input.approver_rid:
raise PermissionError("当前用户不是指定的审批人")
# 记录审批意见
audit = ctx.platform.objects.create(
object_type="AuditTrail",
properties={
"request_rid": input.request_rid,
"approver_rid": input.approver_rid,
"decision": input.decision,
"comment": input.comment,
"timestamp": ctx.now().isoformat(),
}
)
# 处理审批决定
if input.decision == "reject":
ctx.platform.objects.update(
rid=input.request_rid,
properties={"status": "rejected", "completed_at": ctx.now().isoformat()}
)
return ApprovalOutput(
status="rejected",
is_final=True,
audit_trail_rid=audit.rid,
)
# 查找下一级审批人
approval_chain = ctx.platform.oql.execute(f"""
FIND ApprovalStep
TRAVERSE belongs_to -> ApprovalFlow
WHERE ApprovalFlow.type = '{request.flow_type}'
SELECT step_order, approver_role, min_amount
ORDER BY step_order ASC
""")
current_step = next(
(s for s in approval_chain if s.step_order == request.current_step), None
)
next_step = next(
(s for s in approval_chain if s.step_order == request.current_step + 1), None
)
if next_step and request.amount >= next_step.min_amount:
# 需要下一级审批
next_approver = ctx.platform.oql.execute(f"""
FIND Employee WHERE role = '{next_step.approver_role}' LIMIT 1
""")
ctx.platform.objects.update(
rid=input.request_rid,
properties={
"current_step": request.current_step + 1,
"current_approver_rid": next_approver[0].rid if next_approver else None,
"status": "pending_approval",
}
)
return ApprovalOutput(
status="pending_next_approval",
next_approver=next_approver[0].rid if next_approver else None,
is_final=False,
audit_trail_rid=audit.rid,
)
else:
# 审批完成
ctx.platform.objects.update(
rid=input.request_rid,
properties={"status": "approved", "completed_at": ctx.now().isoformat()}
)
return ApprovalOutput(
status="approved",
is_final=True,
audit_trail_rid=audit.rid,
)
#Key Takeaways
- 函数即服务:自定义函数通过 gRPC 暴露,可被 Action、Pipeline、Rule、Dashboard 调用
- 类型安全:使用 Pydantic v2 定义输入输出 Schema,确保类型校验
- 可测试性:通过 Mock Context 进行单元测试,通过 TestPlatform 进行集成测试
- 版本管理:支持多版本共存、灰度发布和快速回滚
- 性能控制:内置缓存、并发限制和超时机制
- Ontology 原生:函数内可直接访问对象、关系和 OQL 查询
#Next Article
下一篇:S12-09 指标开发指南 — 学习如何定义和计算业务指标,构建指标体系。
Tags: 自定义函数 Python gRPC 函数即服务 Pydantic coomia-dip