3 Approaches to Integrating LLMs into Enterprise Backends: Which One to Choose?
Integrating an LLM into an ERP, CRM, or internal helpdesk system is never as simple as sending a prompt and receiving text. In practice, developers usually weigh three main approaches:
- Direct API Calls (OpenAI SDK / REST): Writing raw code, manually parsing JSON schemas, and implementing function-calling loops by hand.
- Using LangChain: A massive ecosystem with countless out-of-the-box integrations. However, its heavy layers of abstraction often turn debugging and schema validation into a nightmare as systems scale up.
- Using Microsoft Semantic Kernel (SK): An open-source framework built with enterprise architectural principles in mind: robust dependency injection, strong type safety, and clear, transparent plugin orchestration.
A Practical Comparison of the 3 Options
1. Raw API Calls: Fast to Start, Painful to Scale
The biggest advantage is being lightweight. You have full control over every single byte of the request payload without installing heavy dependencies.
However, this approach is only practical with a handful of tools. Once your system scales to 20–30 business functions, numerous challenges emerge: retry logic, rate limits, context memory, and prompt orchestration. The codebase quickly turns into an unmanageable bowl of spaghetti.
2. LangChain: Great for Prototyping, Risky for Production
LangChain is fantastic for prototyping or throwing together a basic RAG pipeline in a couple of days.
Yet when deployed in mission-critical enterprise environments running 24/7, development teams frequently run into issues due to frequent breaking changes across releases. Furthermore, its implicit automated reasoning makes it challenging to maintain ISO-compliant audit logs or trace security vulnerabilities.
3. Microsoft Semantic Kernel: Standardized for Enterprise
Semantic Kernel follows conventional software engineering principles: explicit, predictable, and tightly controlled. With its Native Plugins architecture, you can turn standard Python functions into tools that agents can safely invoke using just a few lines of type hints.
- Pros: Leverages Pydantic and standard Python typing, seamlessly supporting both Semantic Functions (prompt templates) and Native Functions (pure Python code). Writing unit tests with pytest is effortless, and it integrates cleanly into microservices architectures.
- Cons: Python SDK documentation lags slightly behind the C#/.NET version. The third-party extension ecosystem and community are also smaller compared to LangChain.
When Should You Choose Semantic Kernel?
Need a quick demo ready for leadership tomorrow? Go with LangChain or direct API calls.
However, if you are building an agent that interacts directly with core business data—such as deducting wallet balances, updating hundreds of database rows, or dispatching order confirmation emails—Semantic Kernel is a much safer choice thanks to its strict parameter validation.
In a real-world ERP project with over 40 internal APIs, our team encountered instances where an agent mismatched data types between strings and integers. Because Semantic Kernel validated the schema directly from the type hints, it caught and blocked malformed queries before they ever reached the database.
Step-by-Step: Implementing Semantic Kernel and Native Plugins
Step 1: Set Up the Environment
Semantic Kernel requires Python 3.10 or higher. Create a clean virtual environment and install the required packages:
python3 -m venv venv
source venv/bin/activate
pip install semantic-kernel python-dotenv
Step 2: Build a Native Plugin with Business Logic
A Native Plugin is simply a standard Python class. You expose functions to the AI using the @kernel_function decorator.
Important note: Always provide clear docstrings and comprehensive type hints using Annotated. Semantic Kernel uses these exact descriptions to generate the function definitions sent to the model.
from typing import Annotated
from semantic_kernel.functions import kernel_function
class InventoryPlugin:
"""Plugin for looking up and updating internal warehouse inventory."""
@kernel_function(
name="check_stock",
description="Check the current stock quantity for a given product SKU."
)
def check_stock(
self,
sku: Annotated[str, "The product SKU to look up, e.g., SKU-101"]
) -> str:
# Mock internal database query
mock_db = {
"SKU-101": 15,
"SKU-202": 0,
"SKU-303": 42
}
quantity = mock_db.get(sku.upper(), None)
if quantity is None:
return f"Product SKU {sku} was not found in the inventory system."
return f"Product {sku} currently has {quantity} units in stock."
@kernel_function(
name="create_restock_ticket",
description="Create a restock purchase order ticket for out-of-stock items."
)
def create_restock_ticket(
self,
sku: Annotated[str, "The product SKU that needs restocking"],
quantity: Annotated[int, "Proposed quantity to restock, e.g., 50"]
) -> str:
return f"Successfully created restock ticket #TK-{sku}-99 for {quantity} units."
Step 3: Initialize the Kernel and Enable Auto Function Invocation
Create a main.py file. In this example, we use OpenAI’s GPT-4o-mini, but you can seamlessly switch to an Azure OpenAI endpoint simply by swapping the connector:
import asyncio
import os
from dotenv import load_dotenv
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from inventory_plugin import InventoryPlugin
load_dotenv()
async def main():
# 1. Initialize orchestrating Kernel
kernel = Kernel()
# 2. Register LLM service
chat_service = OpenAIChatCompletion(
service_id="enterprise_agent",
ai_model_id="gpt-4o-mini",
api_key=os.getenv("OPENAI_API_KEY")
)
kernel.add_service(chat_service)
# 3. Add Native Plugin to Kernel
kernel.add_plugin(InventoryPlugin(), plugin_name="inventory")
# 4. Enable automatic function calling (Auto Invocation)
execution_settings = OpenAIChatPromptExecutionSettings(
service_id="enterprise_agent",
temperature=0.2,
function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=True)
)
# 5. Execute prompt requiring two sequential function calls
user_prompt = "Is SKU-202 in stock? If it's out of stock, please create a restock ticket for 50 units."
print(f"User: {user_prompt}\n")
response = await kernel.invoke_prompt(
prompt=user_prompt,
settings=execution_settings
)
print(f"Agent:\n{response}")
if __name__ == "__main__":
asyncio.run(main())
4 Battle-Tested Best Practices for Production
- Never skip type hints: LLMs cannot guess data types reliably. Always use explicit
Annotated[type, "description"]and strictly avoidAny, which encourages the AI to generate arbitrary parameters. - Mitigate risk with FunctionInvocationFilters: Never grant an agent unrestricted write access. Semantic Kernel lets you attach filters prior to every function execution to enforce role-based access control (RBAC) or require human-in-the-loop approvals for sensitive operations such as money transfers or account deletions.
- Ensure idempotency: Network blips or incomplete context can cause an LLM to trigger a tool twice. Any state-mutating function (POST/UPDATE) should require an
idempotency_keyto prevent duplicate order creation. - Catch all exceptions inside Native Functions: Never let an unhandled database query error crash the execution pipeline. Wrap logic in
try/exceptblocks and return clear, human-readable error messages (such as “Error: Inventory service temporarily disconnected, please try again later”). The AI will process this feedback and gracefully communicate the issue back to the end user.

