Skip to content

Commit cb102c4

Browse files
docs: add decorator blog post (langfuse#545)
--------- Co-authored-by: Hassieb Pakzad <hassieb.pakzad@gmail.com>
1 parent 7e9ea15 commit cb102c4

File tree

3 files changed

+216
-1
lines changed

3 files changed

+216
-1
lines changed
+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
title: "Trace complex LLM applications with the Langfuse decorator (Python)"
3+
date: 2024/04/24
4+
description: When building RAG or agents, lots of LLM calls and non-LLM inputs feeds into the final output. The Langfuse decorator allows you to trace and evaluate holistically.
5+
ogImage: /images/changelog/2024-03-24-python-decorator.png
6+
tag: integration
7+
author: Marc
8+
---
9+
10+
import { BlogHeader } from "@/components/blog/BlogHeader";
11+
12+
<BlogHeader
13+
title="Trace complex LLM applications with the Langfuse decorator (Python)"
14+
description="When building RAG or agents, lots of LLM calls and non-LLM inputs feeds into the final output. The Langfuse decorator allows you to trace and evaluate holistically."
15+
date="April 24, 2024"
16+
authors={["marcklingen", "hassiebpakzad"]}
17+
/>
18+
19+
When we initially built complex agents for web scraping and code generation while in Y Combinator, we quickly recognized the need for new LLM-focused observability to truly understand how our applications produced their outputs. It wasn't just about the LLM calls, but also the retrieval steps, chaining of separate calls, function calling, obtaining the right JSON output, and various API calls that the agents should perform behind the scenes. In the end, all these steps led to them breaking many times on our initial users.
20+
21+
This insight prompted us to start working on Langfuse. As many teams were experimenting, we prioritized easy integrations with frameworks like LangChain and LlamaIndex, as well as wrapping the OpenAI SDK to log individual LLM calls. As applications become increasingly complex and agents are deployed in production, the ability to trace complex applications is even more crucial.
22+
23+
[Traces](/docs/tracing) are at the core of the Langfuse platform. Until now, generating a trace was straightforward if you used a framework. However, if you didn't, you had to use the low-level SDKs to manually create and nest trace objects, which added verbose instrumentation code to a project. Inspired by the developer experience of tools we admire, such as Sentry and Modal, we created the `@observe()` decorator for Langfuse to make tracing your Python code as simple as possible. Note: we plan to revamp our JS/TS tracing as well.
24+
25+
This post is a deep dive into the `@observe()` decorator, our design objectives, challenges we faced when implementing it, and how it can help you trace complex LLM applications.
26+
27+
## Goals
28+
29+
When starting to work on the decorator, we wanted to make everything simple that's complex when manually instrumenting your code. Consider this real-world trace of a complex agent, where maintaining the nesting hierarchy manually using the low-level SDK used to come with additional complexity and boilerplate code.
30+
31+
_Example of a nested trace with multiple LLM, non-LLM calls and a LangChain chain_
32+
33+
<Video
34+
src="/images/blog/python-decorator/complex-trace.mp4"
35+
gifStyle
36+
aspectRatio={16 / 9.93}
37+
/>
38+
39+
The goal of the `@observe()` decorator is to abstract away the complexity of creating and nesting traces and spans, and to make it easy to trace complex applications.
40+
41+
The decorator should:
42+
43+
- Trace all function calls and their outputs in a single trace while maintaining the nesting hierarchy
44+
- Be easy to use and require minimal changes to your code
45+
- Automatically capture the function name, arguments, return value, streamed completions, exceptions, execution time, and nesting of functions
46+
- Be fully compatible with the native [Langfuse integrations](/docs/integrations) for LangChain, LlamaIndex, and the OpenAI SDK
47+
- Encourage reusable abstractions across an LLM-based application without needing to consider how to pass trace objects around
48+
- Support async environments for our many users that run performance-optimized LLM apps
49+
50+
## Design decisions
51+
52+
1. The decorator reuses the low-level SDK to create traces and asynchronously batch them to the Langfuse API. This implementation was derived from the PostHog SDKs and is [tested](/guides/cookbook/langfuse_sdk_performance_test) to have little to no impact on the performance of your application.
53+
2. The decorator maintains a call stack internally that keeps track of nested function calls to reflect the observation hierarchy in the trace.
54+
3. To be async-safe, the decorator leverages [Python Contextvars](https://docs.python.org/3/library/contextvars.html) for managing its state.
55+
4. The same `observe()` decorator is used to create a trace (outermost decorated function) and to add spans to the trace (inner decorated functions). This way, functions can be used in multiple traces without needing to be strictly a "trace" or a "span" function.
56+
57+
## Limitation: Python Contextvars and ThreadPoolExecutors
58+
59+
The power of observability is most visible in complex applications with production workloads. Async and concurrent environments are common in these applications, and the decorator should work seamlessly in these environments. The decorator uses Python's `contextvars` to store the current trace context and to ensure that the observations are correctly associated with the current execution context. This allows you to use the decorator in reliably in async functions.
60+
61+
However, an important exception are Python's ThreadPoolExecutors and ProcessPoolExecutors. The decorator will not work correctly in these environments, as the `contextvars` are not correctly copied to the new threads or processes. There is an [existing issue](https://github.com/python/cpython/pull/9688#issuecomment-544304996) in Python's standard library and a [great explanation](https://github.com/tiangolo/fastapi/issues/2776#issuecomment-776659392) in the fastapi repo that discusses this limitation.
62+
63+
In short, the decorator will work correctly in async environments, but not in ThreadPoolExecutors or ProcessPoolExecutors.
64+
65+
## Before and after
66+
67+
### Status quo: Low-level SDK
68+
69+
The low-level SDK is very flexible but it is also _very_ verbose and requires passing of Langfuse objects.
70+
71+
```python /langfuse = Langfuse()/ /span = trace.span(name="story")/ /trace_id=trace.id/ /parent_observation_id=span.id/ /span.end(output=output)/ /trace = langfuse.trace("main")/ /trace/
72+
from langfuse import Langfuse
73+
from langfuse.openai import openai # OpenAI integration
74+
75+
langfuse = Langfuse()
76+
77+
def story(trace):
78+
span = trace.span(name="story")
79+
output = openai.chat.completions.create(
80+
model="gpt-3.5-turbo",
81+
max_tokens=100,
82+
messages=[
83+
{"role": "system", "content": "You are a great storyteller."},
84+
{"role": "user", "content": "Once upon a time in a galaxy far, far away..."}
85+
],
86+
trace_id=trace.id,
87+
parent_observation_id=span.id
88+
).choices[0].message.content
89+
span.end(output=output)
90+
return output
91+
92+
93+
def main():
94+
trace = langfuse.trace("main")
95+
return story(trace)
96+
```
97+
98+
### `@observe()` decorator to the rescue
99+
100+
All complexity is abstracted away and you can focus on your business logic. The OpenAI SDK wrapper is aware that it is run within a decorated function and automatically adds its logs to the trace.
101+
102+
```python /@observe()/
103+
from langfuse.decorators import observe
104+
from langfuse.openai import openai # OpenAI integration
105+
106+
@observe()
107+
def story():
108+
return openai.chat.completions.create(
109+
model="gpt-3.5-turbo",
110+
max_tokens=100,
111+
messages=[
112+
{"role": "system", "content": "You are a great storyteller."},
113+
{"role": "user", "content": "Once upon a time in a galaxy far, far away..."}
114+
],
115+
).choices[0].message.content
116+
117+
@observe()
118+
def main():
119+
return story()
120+
121+
main()
122+
```
123+
124+
<Frame fullWidth>
125+
![Simple OpenAI decorator
126+
trace](/images/docs/python-decorator-simple-trace.png)
127+
</Frame>
128+
129+
## Interoperability
130+
131+
The decorator completely replaces the need to use the low-level SDK. It allows for the creation and manipulation of traces, and you can add custom scores and evaluations to these traces as well. Have a look at the extensive [documentation](/docs/sdk/python/decorators) for more details.
132+
133+
Langfuse is natively integrated with LangChain, LlamaIndex, and the OpenAI SDK and the decorator is [fully compatible](/docs/sdk/python/example#interoperability-with-other-integrations) with these integrations. As a result, you can, in a single trace, use the decorator on the outermost function, decorate function calls and API calls that are non-LLM related, and use the native instrumentation for the OpenAI SDK, LangChain and Llama Index.
134+
135+
Example ([cookbook](/guides/cookbook/python_decorators#interoperability-with-other-integrations)):
136+
137+
```python
138+
from langfuse.openai import openai
139+
from langfuse.decorators import observe
140+
141+
@observe()
142+
def openai_fn(calc: str):
143+
res = openai.chat.completions.create(
144+
model="gpt-3.5-turbo",
145+
messages=[
146+
{"role": "system", "content": "You are a very accurate calculator. You output only the result of the calculation."},
147+
{"role": "user", "content": calc}],
148+
)
149+
return res.choices[0].message.content
150+
151+
@observe()
152+
def llama_index_fn(question: str):
153+
# Set callback manager for LlamaIndex, will apply to all LlamaIndex executions in this function
154+
langfuse_handler = langfuse_context.get_current_llama_index_handler()
155+
Settings.callback_manager = CallbackManager([langfuse_handler])
156+
157+
# Run application
158+
index = VectorStoreIndex.from_documents([doc1,doc2])
159+
response = index.as_query_engine().query(question)
160+
return response
161+
162+
@observe()
163+
def langchain_fn(person: str):
164+
# Get Langchain Callback Handler scoped to the current trace context
165+
langfuse_handler = langfuse_context.get_current_langchain_handler()
166+
167+
# Pass handler to invoke method of chain/agent
168+
chain.invoke({"person": person}, config={"callbacks":[langfuse_handler]})
169+
170+
@observe()
171+
def main():
172+
output_openai = openai_fn("5+7")
173+
output_llamaindex = llama_index_fn("What did he do growing up?")
174+
output_langchain = langchain_fn("Feynman")
175+
176+
return output_openai, output_llamaindex, output_langchain
177+
178+
main();
179+
```
180+
181+
## Outlook
182+
183+
The decorator drives open tracing for teams that don't want to commit to a single application framework or ecosystem, but want to easily switch between frameworks while relying on Langfuse as a single [platform](/docs) for all experimentation, observability and evaluation needs.
184+
185+
Roadmap: The decorator is currently only available for Python. We will add a similar implementation for JS/TS.
186+
187+
## Add-on
188+
189+
If you want to built complex applications while being able to easily switch between models, we strongly recommend using this stack:
190+
191+
- Langfuse Decorator for tracing
192+
- Langfuse OpenAI SDK Wrapper for automatic instrumentation of OpenAI calls
193+
- LiteLLM Proxy for standardization of 100+ models on the OpenAI API
194+
195+
Have a look at [this cookbook](/guides/cookbook/integration_litellm_proxy) to see an end-to-end example – we really think you'll like this stack and there are lots of teams in the Langfuse Community who built on top of it.
196+
197+
## Thank you
198+
199+
Thank you to everyone who tested the decorator during the beta phase and provided feedback. We've received several Gists from community members showcasing their own decorator implementations built using Langfuse before the decorator was released as an official integration. We're excited to see what you create with it!
200+
201+
## Get Started
202+
203+
Run the end-to-end cookbook on your Langfuse traces or learn more about model-based evals in Langfuse.
204+
205+
import { FileCode, BookOpen } from "lucide-react";
206+
207+
<Cards num={3}>
208+
<Card title="Docs" href="/docs/sdk/python/decorators" icon={<BookOpen />} />
209+
<Card
210+
title="Example notebook"
211+
href="/docs/sdk/python/example"
212+
icon={<FileCode />}
213+
/>
214+
</Cards>
Binary file not shown.

theme.config.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
AvailabilityBanner,
2626
AvailabilitySidebar,
2727
} from "./components/availability";
28-
import { CloudflareVideo } from "./components/Video";
28+
import { CloudflareVideo, Video } from "./components/Video";
2929

3030
const config: DocsThemeConfig = {
3131
logo: <Logo />,
@@ -227,6 +227,7 @@ const config: DocsThemeConfig = {
227227
AvailabilityBanner,
228228
Callout,
229229
CloudflareVideo,
230+
Video,
230231
},
231232
banner: {
232233
key: "launch-week",

0 commit comments

Comments
 (0)